managers/share.js

/* global ClipboardItem, Blob */
import { Storage } from '../storage';
import SupportedChannel from '../models/invites/SupportedChannel';
import SDK from '../sdk';
import Invites from '../Invites';

/**
 * @class SmartShare
 * @hideconstructor
 */
export default class SmartShare {
    /**
     * Create and display a Smart Share.
     *
     * @memberOf SmartShare
     * @param {Object} params
     * @param {string} params.target - ID of the div where the Smart Share will be shown.
     * @param {Object} params.options - Customization options.
     * @param {string} params.options.inviteText - Text to include with the invite link.
     * @param {string} params.options.emailSubject - E-mail subject when sharing via e-mail.
     * @param {string} params.options.twitterVia - Twitter account to include at the end of the tweet.
     * @param {Object.<string, string>} params.customData - Custom data attached to the Smart Invite.
    */
  static create (params) {
    params = params || {};

    // DOM element the widget will be rendered to.
    const target = params.target;
    // Make sure the container element exists.
    if (typeof params.target !== 'string' || !document.getElementById(params.target)) {
      console.error("Container for Smart Share with id '%s' doesn't exist.", params.target);
      return;
    }

    SmartShare.options = params.options || {};
    SmartShare.customData = params.customData || {};

    SmartShare._render(target);
  }

  /** @ignore */
  static _render (target) {
    if (Storage.getIdentity()) {
      const targetEl = document.getElementById(target);
      // Include copy btn in Safari only if Clipboard API is available
      const includeCopy = !navigator.vendor.match(/apple/i) ||
        (navigator.clipboard &&
          navigator.clipboard.write);

      targetEl.innerHTML = SmartShare.getHTML(includeCopy);

      if (targetEl.querySelectorAll('button').length) {
        targetEl.querySelectorAll('button')
          .forEach((el) => {
            el.addEventListener('click', (e) => {
              SmartShare.onButtonClicked(e);
            });
          });
      }
    } else {
      console.error('User must be authenticated to render the SmartShare bar');
    }
  }

  /** @ignore */
  static getHTML (includeCopy) {
    const copyItem = `<li>
      <button id="gs-share-copy">
        Copy
      </button>
    </li>`;

    return `
      <ul id="smart-share">
        <li>
          <button id="gs-share-facebook">
            Facebook
          </button>
        </li>
        <li>
          <button id="gs-share-email">
            E-mail
          </button>
        </li>
        <li>
          <button id="gs-share-twitter">
            Twitter
          </button>
        </li>
        ${includeCopy ? copyItem : ''}
      </ul>
      <style>
          #smart-share {
            display: flex;
            list-style-type: none;
            margin: 0;
            padding:0;
            grid-auto-flow: column;
            grid-column-gap: 4px;
            grid-row-gap: 4px;
            grid-auto-columns: min-content;
            flex-wrap: wrap;
          }

          #smart-share button {
            width: 140px;
            white-space: nowrap;
            border: 0;
            color: white;
            cursor: pointer;
            padding: 8px 16px 8px 30px;
            font-size: 18px;
            line-height: 1;
            background-size: 18px;
            background-repeat: no-repeat;
            background-position: 8px center;
          }

          #gs-share-facebook {
            background-color: #38529a;
            background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIGhlaWdodD0iMWVtIiB3aWR0aD0iMWVtIiB2aWV3Qm94PSIwIDAgNDAgNDAiPgogIDxnPgogICAgPHBhdGggZD0ibTIxLjcgMTYuN2g1djVoLTV2MTEuNmgtNXYtMTEuNmgtNXYtNWg1di0yLjFjMC0yIDAuNi00LjUgMS44LTUuOSAxLjMtMS4zIDIuOC0yIDQuNy0yaDMuNXY1aC0zLjVjLTAuOSAwLTEuNSAwLjYtMS41IDEuNXYzLjV6Ij48L3BhdGg+CiAgPC9nPgo8L3N2Zz4KCg==)
          }

          #gs-share-twitter {
            background-color: #4da6e9;
            background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIGhlaWdodD0iMWVtIiB3aWR0aD0iMWVtIiB2aWV3Qm94PSIwIDAgNDAgNDAiPgogIDxnPgogICAgPHBhdGggZD0ibTMxLjUgMTEuN2MxLjMtMC44IDIuMi0yIDIuNy0zLjQtMS40IDAuNy0yLjcgMS4yLTQgMS40LTEuMS0xLjItMi42LTEuOS00LjQtMS45LTEuNyAwLTMuMiAwLjYtNC40IDEuOC0xLjIgMS4yLTEuOCAyLjctMS44IDQuNCAwIDAuNSAwLjEgMC45IDAuMiAxLjMtNS4xLTAuMS05LjQtMi4zLTEyLjctNi40LTAuNiAxLTAuOSAyLjEtMC45IDMuMSAwIDIuMiAxIDMuOSAyLjggNS4yLTEuMS0wLjEtMi0wLjQtMi44LTAuOCAwIDEuNSAwLjUgMi44IDEuNCA0IDAuOSAxLjEgMi4xIDEuOCAzLjUgMi4xLTAuNSAwLjEtMSAwLjItMS42IDAuMi0wLjUgMC0wLjkgMC0xLjEtMC4xIDAuNCAxLjIgMS4xIDIuMyAyLjEgMyAxLjEgMC44IDIuMyAxLjIgMy42IDEuMy0yLjIgMS43LTQuNyAyLjYtNy42IDIuNi0wLjcgMC0xLjIgMC0xLjUtMC4xIDIuOCAxLjkgNiAyLjggOS41IDIuOCAzLjUgMCA2LjctMC45IDkuNC0yLjcgMi44LTEuOCA0LjgtNC4xIDYuMS02LjcgMS4zLTIuNiAxLjktNS4zIDEuOS04LjF2LTAuOGMxLjMtMC45IDIuMy0yIDMuMS0zLjItMS4xIDAuNS0yLjMgMC44LTMuNSAxeiI+PC9wYXRoPgogIDwvZz4KPC9zdmc+Cgo=);
          }

          #gs-share-email {
            background-color: #ec4134;
            background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIGhlaWdodD0iMWVtIiB3aWR0aD0iMWVtIiB2aWV3Qm94PSIwIDAgNDAgNDAiPgogIDxnPgogICAgPHBhdGggZD0ibTMzLjQgMTMuNHYtMy40bC0xMy40IDguNC0xMy40LTguNHYzLjRsMTMuNCA4LjJ6IG0wLTYuOHExLjMgMCAyLjMgMS4xdDAuOSAyLjN2MjBxMCAxLjMtMC45IDIuM3QtMi4zIDEuMWgtMjYuOHEtMS4zIDAtMi4zLTEuMXQtMC45LTIuM3YtMjBxMC0xLjMgMC45LTIuM3QyLjMtMS4xaDI2Ljh6Ij48L3BhdGg+CiAgPC9nPgo8L3N2Zz4KCg==);
          }

          #gs-share-copy {
            background-color: #f6bf42;
            background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMjEwLjEwNyAyMTAuMTA3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMTAuMTA3IDIxMC4xMDc7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCgk8Zz4NCgkJPHBhdGggZD0iTTE2OC41MDYsMEg4MC4yMzVDNjcuNDEzLDAsNTYuOTgxLDEwLjQzMiw1Ni45ODEsMjMuMjU0djIuODU0aC0xNS4zOA0KCQkJYy0xMi44MjIsMC0yMy4yNTQsMTAuNDMyLTIzLjI1NCwyMy4yNTR2MTM3LjQ5MmMwLDEyLjgyMiwxMC40MzIsMjMuMjU0LDIzLjI1NCwyMy4yNTRoODguMjcxDQoJCQljMTIuODIyLDAsMjMuMjUzLTEwLjQzMiwyMy4yNTMtMjMuMjU0VjE4NGgxNS4zOGMxMi44MjIsMCwyMy4yNTQtMTAuNDMyLDIzLjI1NC0yMy4yNTRWMjMuMjU0QzE5MS43NiwxMC40MzIsMTgxLjMyOCwwLDE2OC41MDYsMHoNCgkJCU0xMzguMTI2LDE4Ni44NTRjMCw0LjU1MS0zLjcwMyw4LjI1NC04LjI1Myw4LjI1NEg0MS42MDFjLTQuNTUxLDAtOC4yNTQtMy43MDMtOC4yNTQtOC4yNTRWNDkuMzYxDQoJCQljMC00LjU1MSwzLjcwMy04LjI1NCw4LjI1NC04LjI1NGg4OC4yNzFjNC41NTEsMCw4LjI1MywzLjcwMyw4LjI1Myw4LjI1NFYxODYuODU0eiBNMTc2Ljc2LDE2MC43NDYNCgkJCWMwLDQuNTUxLTMuNzAzLDguMjU0LTguMjU0LDguMjU0aC0xNS4zOFY0OS4zNjFjMC0xMi44MjItMTAuNDMyLTIzLjI1NC0yMy4yNTMtMjMuMjU0SDcxLjk4MXYtMi44NTQNCgkJCWMwLTQuNTUxLDMuNzAzLTguMjU0LDguMjU0LTguMjU0aDg4LjI3MWM0LjU1MSwwLDguMjU0LDMuNzAzLDguMjU0LDguMjU0VjE2MC43NDZ6Ii8+DQoJPC9nPg0KPC9zdmc+DQo=);
          }
      </style>
    `;
  }

  /** @ignore */
  static getChannel (id) {
    const prefix = 'gs-share-';
    return id.startsWith(prefix)
      ? id.substr(prefix.length)
      : null;
  }

  /** @ignore */
  static copyToClipboard (txt) {
    // Get current selection
    const selected =
      document.getSelection().rangeCount > 0
        ? document.getSelection().getRangeAt(0)
        : false;

    const txtArea = document.createElement('textarea');
    txtArea.value = txt;
    txtArea.setAttribute('readonly', '');
    txtArea.style.position = 'absolute';
    txtArea.style.left = '-9999px';
    document.body.appendChild(txtArea);

    txtArea.select();
    document.execCommand('copy');
    document.body.removeChild(txtArea);

    if (selected) {
      document.getSelection().removeAllRanges();
      // Select back previously selected text
      document.getSelection().addRange(selected);
    }
  }

  /** @ignore */
  static onButtonClicked (e) {
    const channel = SmartShare.getChannel(e.target.id);

    if (channel) {
      e.target.disabled = true;

      SmartShare.shareLinkViaChannel(channel)
        .then(() => {
            // Use ClipboardAPI in Safari
            if (channel === 'copy') {
              SmartShare.copySuccessful(e);
            }
        })
        .finally(() => { e.target.disabled = false; })
    }
  }

  /** @ignore */
  static shareLinkViaChannel(channel, options) {
    if (channel) {
      options = options || SmartShare.options || {};
      const windowFeatures = 'resizable,scrollbars,status,left=200,top=200,width=720,height=520';
      let inviteText = options.inviteText
        ? options.inviteText + ' '
        : '';
      let subject = options.emailSubject
        ? options.emailSubject
        : '';
      const via = options.twitterVia
        ? `&via=${encodeURI(options.twitterVia)}`
        : '';
      const customData = options.customData || null;

      if (inviteText.indexOf('[APP_INVITE_URL]') < 0) {
        inviteText += '[APP_INVITE_URL]';
      }


      // Replace placeholders
      const defaultContent = Invites.getDefaultContent(channel);
      const user = SDK.getCurrentUser();
      const name = user
        ? user.displayName
        : '';

      inviteText = inviteText
        .replace(/\[APP_INVITE_TEXT\]/g, defaultContent.text || '');
      subject = subject
        .replace(/\[APP_INVITE_SUBJECT\]/g, defaultContent.subject || '');
      inviteText = inviteText
        .replace(/\[USER_NAME\]/g, name || '');
      subject = subject
        .replace(/\[USER_NAME\]/g, name || '');
      inviteText = inviteText
        .replace(/\[APP_NAME\]/g, SDK.superProps.appName || '');
      subject = subject
        .replace(/\[APP_NAME\]/g, SDK.superProps.appName || '');

      // Use ClipboardAPI in Safari
      if (channel === 'copy' &&
        navigator.vendor.match(/apple/i)) {
        const linkPromise = SmartShare.getLink(channel, customData);
        const clipboardPromise = new Promise((resolve) => {
          linkPromise
            .then((url) => {
              resolve(
                new Blob(
                  [inviteText.replace(/\[APP_INVITE_URL\]/g, url)],
                  {
                    type: 'text/plain'
                  }
                )
              );

              return url;
            })
            .catch(console.error);
        });

        navigator.clipboard.write([
          new ClipboardItem({
            'text/plain': clipboardPromise
          })
        ]);

        return linkPromise;
      }

      return SmartShare.getLink(channel, customData)
        .then((url) => {
          const shareTxt = inviteText.replace(/\[APP_INVITE_URL\]/g, '');
          const fullTxt = inviteText.replace(/\[APP_INVITE_URL\]/g, url);

          switch (channel) {
            case SupportedChannel.Facebook:
              window.open(`https://www.facebook.com/sharer.php?display=popup&u=${encodeURI(url)}&quote=${encodeURI(shareTxt)}`, 'SmartShareFacebookPost', windowFeatures);
              break;
            case SupportedChannel.Twitter:
              window.open(`https://twitter.com/intent/tweet?text=${encodeURI(shareTxt)}${via}&original_referer=${encodeURI(window.location.href)}&url=${encodeURI(url)}`, 'SmartShareTweet', windowFeatures);
              break;
            case SupportedChannel.Email:
              window.location.href = `mailto:?subject=${encodeURI(subject)}&body=${encodeURI(fullTxt)}`;
              break;
            case SupportedChannel.CopyToClipboard:
              SmartShare.copyToClipboard(fullTxt);
              break;
            default:
              console.error(`Channel ${channel} is not supported`);
          }

          return url;
        })
        .catch(console.error);
    }

    return Promise.reject('Channel not provided');
  }

  /** @ignore */
  static copySuccessful (e) {
    var currentText = e.target.innerText;
    e.target.innerText = 'Copied!';
    setTimeout(() => {
      e.target.innerText = currentText;
    }, 1000);
  }

  /** @ignore */
  static getLink (channel, customData) {
    customData = customData || SmartShare.customData || {};
    return Invites.createURL({
      channel,
      ...customData
    });
  }
}