nicolasbeauvais / vue-social-sharing

A renderless Vue.js component for sharing links to social networks, compatible with SSR
https://nicolasbeauvais.github.io/vue-social-sharing/
MIT License
1.39k stars 196 forks source link

Nuxt 3 compostion compatible #361

Open ssglopes opened 4 months ago

ssglopes commented 4 months ago

Considering the list of addressed issues I decided to convert this package myself into a component that works with nuxt 3. Credits go to the original author of this package. I updated it to fix some reported bugs that I noticed and to have a version working with Nuxt 3 and Vue 3. I did not make a new repo for this, everything needed is in the below component. I just post here the component for others to use as they see fit.

// componentName = BaseShareNetwork.vue
<script setup lang="ts">
import { computed, ref } from 'vue'
const useNetworks = {
  baidu: 'http://cang.baidu.com/do/add?iu=@u&it=@t',
  buffer: 'https://bufferapp.com/add?text=@t&url=@u',
  email: 'mailto:?subject=@t&body=@u%0D%0A@d',
  evernote: 'https://www.evernote.com/clip.action?url=@u&title=@t',
  facebook: 'https://www.facebook.com/sharer/sharer.php?u=@u&title=@t&description=@d&quote=@q&hashtag=@h',
  flipboard: 'https://share.flipboard.com/bookmarklet/popout?v=2&url=@u&title=@t',
  hackernews: 'https://news.ycombinator.com/submitlink?u=@u&t=@t',
  instapaper: 'http://www.instapaper.com/edit?url=@u&title=@t&description=@d',
  line: 'http://line.me/R/msg/text/?@t%0D%0A@u%0D%0A@d',
  linkedin: 'https://www.linkedin.com/sharing/share-offsite/?url=@u',
  messenger: 'fb-messenger://share/?link=@u',
  odnoklassniki: 'https://connect.ok.ru/dk?st.cmd=WidgetSharePreview&st.shareUrl=@u&st.comments=@t',
  pinterest: 'https://pinterest.com/pin/create/button/?url=@u&media=@m&description=@t',
  pocket: 'https://getpocket.com/save?url=@u&title=@t',
  quora: 'https://www.quora.com/share?url=@u&title=@t',
  reddit: 'https://www.reddit.com/submit?url=@u&title=@t',
  skype: 'https://web.skype.com/share?url=@t%0D%0A@u%0D%0A@d',
  sms: 'sms:?body=@t%0D%0A@u%0D%0A@d',
  stumbleupon: 'https://www.stumbleupon.com/submit?url=@u&title=@t',
  telegram: 'https://t.me/share/url?url=@u&text=@t%0D%0A@d',
  tumblr: 'https://www.tumblr.com/share/link?url=@u&name=@t&description=@d',
  x: 'https://x.com/intent/tweet?text=@t&url=@u&hashtags=@h@tu',
  viber: 'viber://forward?text=@t%0D%0A@u%0D%0A@d',
  vk: 'https://vk.com/share.php?url=@u&title=@t&description=@d&image=@m&noparse=true',
  weibo: 'http://service.weibo.com/share/share.php?url=@u&title=@t&pic=@m',
  whatsapp: 'https://api.whatsapp.com/send?text=@t%0D%0A@u%0D%0A@d',
  wordpress: 'https://wordpress.com/press-this.php?u=@u&t=@t&s=@d&i=@m',
  xing: 'https://www.xing.com/social/share/spi?op=share&url=@u&title=@t',
  yammer: 'https://www.yammer.com/messages/new?login=true&status=@t%0D%0A@u%0D%0A@d'
}
let $window = typeof window !== 'undefined' ? window : null
const popupTop = ref(0)
const popupLeft = ref(0)
const popupInterval = ref(null)
const emit = defineEmits(['open:network', 'close:network', 'change:network'])
const props = defineProps({
  /**
   * Name of the network to display.
   */
  network: {
    type: String,
    required: true
  },

  /**
   * URL of the content to share.
   */
  url: {
    type: String,
    required: true
  },

  /**
   * Title of the content to share.
   */
  title: {
    type: String,
    required: true
  },

  /**
   * Description of the content to share.
   */
  description: {
    type: String,
    default: ''
  },

  /**
   * Quote content, used for Facebook.
   */
  quote: {
    type: String,
    default: ''
  },

  /**
   * Hashtags, used for Twitter and Facebook.
   */
  hashtags: {
    type: String,
    default: ''
  },

  /**
   * Twitter user, used for Twitter
   * @var string
   */
  twitterUser: {
    type: String,
    default: ''
  },

  /**
   * Media to share, used for Pinterest
   */
  media: {
    type: String,
    default: ''
  },

  /**
   * Properties to configure the popup window.
   */
  popup: {
    type: Object,
    default: () => ({
      width: 626,
      height: 436
    })
  }
})

/**
 * List of available networks
 */
const networks = computed(() => {
  return useNetworks
})

/**
 * Formatted network name.
 */
const key = computed(() => {
  return props.network.toLowerCase()
})

/**
 * Network sharing raw sharing link.
 */
const rawLink = computed(() => {
  const ua = navigator.userAgent.toLowerCase()

  /**
   * On IOS, SMS sharing link need a special formatting
   * Source: https://weblog.west-wind.com/posts/2013/Oct/09/Prefilling-an-SMS-on-Mobile-Devices-with-the-sms-Uri-Scheme#Body-only
   */
  if (key.value === 'sms' && (ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1)) {
    return networks.value[key.value].replace(':?', ':&')
  }
  return networks.value[key.value]
})

/**
 * Create the url for sharing.
 */
const shareLink = computed(() => {
  let link = rawLink.value
  /**
   * Twitter sharing shouldn't include empty parameter
   * Source: https://github.com/nicolasbeauvais/vue-social-sharing/issues/143
   */
  if (key.value === 'x') {
    if (!props.hashtags.length) link = link.replace('&hashtags=@h', '')
    if (!props.twitterUser.length) link = link.replace('@tu', '')
  }
  return link
    .replace(/@tu/g, '&via=' + encodeURIComponent(props.twitterUser))
    .replace(/@u/g, encodeURIComponent(props.url))
    .replace(/@t/g, encodeURIComponent(props.title))
    .replace(/@d/g, encodeURIComponent(props.description))
    .replace(/@q/g, encodeURIComponent(props.quote))
    .replace(/@h/g, encodedHashtags.value)
    .replace(/@m/g, encodeURIComponent(props.media))
})

/**
 * Encoded hashtags for the current social network.
 */
const encodedHashtags = computed(() => {
  if (key.value === 'facebook' && props.hashtags.length) {
    return '%23' + props.hashtags.split(',')[0]
  }
  return props.hashtags
})

/**
 * Center the popup on multi-screens
 * http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263
 */
const resizePopup = () => {
  const width = $window.innerWidth || (document.documentElement.clientWidth || $window.screenX)
  const height = $window.innerHeight || (document.documentElement.clientHeight || $window.screenY)
  const systemZoom = width / $window.screen.availWidth
  popupLeft.value = (width - props.popup.width) / 2 / systemZoom + ($window.screenLeft !== undefined ? $window.screenLeft : $window.screenX)
  popupTop.value = (height - props.popup.height) / 2 / systemZoom + ($window.screenTop !== undefined ? $window.screenTop : $window.screenY)
}

/**
 * Shares URL in specified network.
 */
const share = () => {
  resizePopup()
  let popupWindow
  // If a popup window already exist, we close it and trigger a change event.
  if (popupWindow && popupInterval.value) {
    clearInterval(popupInterval.value)
    // Force close (for Facebook)
    popupWindow.close()
    doEmit('change')
  }
  popupWindow = $window.open(
      shareLink.value,
      'sharer-' + key.value,
      ',height=' + props.popup.height +
      ',width=' + props.popup.width +
      ',left=' + popupLeft.value +
      ',top=' + popupTop.value +
      ',screenX=' + popupLeft.value +
      ',screenY=' + popupTop.value
  )
  // If popup are prevented (AdBlocker, Mobile App context..), popup.window stays undefined and we can't display it
  if (!popupWindow) return
  popupWindow.focus()
  // Create an interval to detect popup closing event
  popupInterval.value = setInterval(() => {
    if (!popupWindow || popupWindow.closed) {
      clearInterval(popupInterval.value)
      popupWindow = null
      doEmit('close')
    }
  }, 500)
  doEmit('open')
}

/**
 * Touches network and emits click event.
 */
const touch = () => {
  window.open(shareLink.value, '_blank')
  doEmit('open')
}

const doEmit = (action) => {
  emit(`${action}:network`, { 
    network: key.value, 
    share_url: props.url
  })
}
</script>
<template>
  <div :class="`share-network-${key}`" @click="rawLink.substring(0, 4) === 'http' ? share() : touch()"><slot /></div>
</template>

An example on how to use:

      <ClientOnly>
        <BaseShareNetwork
          v-for="(icon, index) in shareOn"
          :key="index"
          :network="icon.platform"
          :url="url.href"
          :title="post.title"
          :description="post.body_short"
          class="!text-black hover:text-lime-500 inline-flex justify-center items-center border bg-slate-50 rounded-full w-[26px] h-[26px]"
          :hashtags="postStore.keywords(post.tags) ?? ''"
          @close:network="sharedOn"
        >
          <BaseIcon :name="icon.platform" class="text-black hover:text-lime-500 text-xs inline" />
        </BaseShareNetwork>
</ClientOnly>
dixiaoping commented 3 months ago

Hello, please tell me, this package of the author should also support vue3.

withgogo commented 2 months ago

@ssglopes Thanks, it works great!

NeutelingsRiedijk commented 1 month ago

@ssglopes excellent job, thanks! Makes it much more easy to customize and maintain like that, just 1 component :)

amadeann commented 1 month ago

If anyone needs it, I rewrote it in a framework-agnostic way, also 1 file: https://github.com/amadeann/social-sharing/blob/master/index.js

Example usage with alpine.js: https://github.com/amadeann/social-sharing/blob/master/index.html

published on npm as well: https://www.npmjs.com/package/@amadeann/social-sharing