lib/components/SharingOptionsLink.vue

Summary

Maintainability
Test Coverage
<script lang="ts">
import querystring from 'querystring-es3'
import reduce from 'lodash/reduce'
import noop from 'lodash/noop'
import get from 'lodash/get'
import { faEnvelope } from '@fortawesome/free-solid-svg-icons/faEnvelope'
import { faTwitter } from '@fortawesome/free-brands-svg-icons/faTwitter'
import { faFacebook } from '@fortawesome/free-brands-svg-icons/faFacebook'
import { faLinkedin } from '@fortawesome/free-brands-svg-icons/faLinkedin'
import { CreateElement, defineComponent, PropType, VNode, VNodeChildren } from 'vue'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'

import Fa from './Fa'

// Popup instance and an interval holder
type Popup = {
  instance: Window | null | undefined
  interval: undefined | ReturnType<typeof setTimeout>
  parent: (Window & typeof globalThis) | null
}
export const $popup: Popup = {
  instance: null,
  interval: undefined,
  parent: typeof window !== 'undefined' ? window : null
}

// Prevent propagation when an event is fired through the given callback
const preventDefault = (callback: Function) => {
  return (event: Event) => {
    event && event.preventDefault()
    callback()
  }
}

type SharingPlatform = {
  base: string
  icon: IconDefinition
  args: {
    [key: string]: string
  }
}
type Platform = 'email' | 'facebook' | 'linkedin' | 'twitter'
// eslint-disable-next-line no-unused-vars
type SharingPlatforms = { [key in Platform]: SharingPlatform }
/**
 * @source https://github.com/bradvin/social-share-urls
 */
export const networks: SharingPlatforms = {
  email: {
    base: 'mailto:?',
    icon: faEnvelope,
    args: {
      subject: 'title',
      body: 'description'
    }
  },
  facebook: {
    base: 'https://www.facebook.com/sharer.php?',
    icon: faFacebook,
    args: {
      u: 'url',
      title: 'title',
      description: 'description',
      hashtag: 'hashtags'
    }
  },
  linkedin: {
    base: 'https://www.linkedin.com/sharing/share-offsite/?',
    icon: faLinkedin,
    args: {
      url: 'url',
      title: 'title',
      summary: 'description'
    }
  },
  twitter: {
    base: 'https://twitter.com/intent/tweet?',
    icon: faTwitter,
    args: {
      url: 'url',
      text: 'title',
      via: 'user',
      hashtags: 'hashtags'
    }
  }
}

/**
 * SharingOptionsLink
 */
export default defineComponent({
  name: 'SharingOptionsLink',
  components: {
    Fa
  },
  props: {
    /**
     * Root element type
     */
    tag: {
      type: String,
      default: 'a'
    },
    /**
     * Social network to use
     */
    network: {
      type: String as PropType<Platform>,
      required: true,
      validator(val: string) {
        return Object.keys(networks).includes(val)
      }
    },
    /**
     * Disable icon
     */
    noIcon: {
      type: Boolean
    },
    /**
     * Shared URL
     */
    url: {
      type: String,
      default: null
    },
    /**
     * Shared text
     */
    title: {
      type: String,
      default: null
    },
    /**
     * Shared description
     */
    description: {
      type: String,
      default: null
    },
    /**
     * Shared image
     */
    media: {
      type: String,
      default: null
    },
    /**
     * Twitter user
     */
    user: {
      type: String,
      default: null
    },
    /**
     * Shared hashtags
     */
    hashtags: {
      type: String,
      default: null
    }
  },
  data() {
    return {
      popup: {
        status: 'no',
        resizable: 'yes',
        toolbar: 'no',
        menubar: 'no',
        scrollbars: 'no',
        location: 'no',
        directories: 'no',
        width: 626,
        height: 436,
        top: 0,
        left: 0,
        screenY: 0,
        screenX: 0
      }
    }
  },
  computed: {
    href(): string {
      return this.base + querystring.stringify(this.query)
    },
    base(): string {
      return get(networks, [this.network, 'base'], '')
    },
    args(): { [key: string]: string } {
      return get(networks, [this.network, 'args'], {})
    },
    icon(): IconDefinition | null {
      return get(networks, [this.network, 'icon'], null)
    },
    query(): any {
      return reduce(
        this.args,
        (obj, prop, param) => {
          // @ts-ignore
          if (this.$props[prop]) {
            // @ts-ignore
            obj[param] = this.$props[prop]
          }
          return obj
        },
        {}
      )
    },
    name(): string {
      return get(networks, [this.network, 'name'], this.network)
    },
    popupParams(): string {
      return querystring.stringify(this.popup).split('&').join(',')
    }
  },
  methods: {
    click(): void {
      this.cleanExistingPopupInstance()
      this.openPopup()
    },
    renderIcon(h: CreateElement): void | VNode | null {
      if (!this.noIcon) {
        return h('fa', { props: { icon: this.icon } })
      }
    },
    openPopup(): void {
      // Create the popup
      $popup.instance = $popup.parent?.open(this.href, 'sharer', this.popupParams)
      $popup.instance?.focus()
      // Watch for popup closing
      $popup.interval = setInterval(this.cleanExistingPopupInterval, 500)
    },
    cleanExistingPopupInstance(): void {
      if ($popup.instance && $popup.interval) {
        clearInterval($popup.interval)
        $popup.interval = undefined
        $popup.instance.close()
      }
    },
    cleanExistingPopupInterval() {
      if ($popup.instance && $popup.instance.closed) {
        clearInterval($popup.interval)
        $popup.interval = undefined
        $popup.instance = null
      }
    },
    hasPopup(): boolean {
      return this.network !== 'email'
    }
  },
  render(h: CreateElement): void | VNode | null {
    const click = this.hasPopup() ? preventDefault(this.click) : noop
    const href = this.href
    const children =
      this.$slots.default || ([this.renderIcon(h), h('span', { class: 'sr-only' }, this.name)] as VNodeChildren)
    return h(this.tag, { attrs: { href }, on: { click } }, children)
  }
})
</script>