UNPKG

@ribajs/bs5

Version:

Bootstrap 5 module for Riba.js

456 lines (405 loc) 12.9 kB
import { Component, TemplateFunction, ScopeBase } from "@ribajs/core"; import { getUrl } from "@ribajs/utils/src/url"; import template from "./bs5-share.component.html?raw"; import labelTemplate from "./bs5-share.label.html?raw"; import { ShareItem, ShareUrlType } from "../../types/index.js"; import { Dropdown } from "@ribajs/bs5"; import { hasChildNodesTrim, copyTextToClipboard, stripHtml, } from "@ribajs/utils"; export interface Scope extends ScopeBase { type: ShareUrlType; title: string; text: string; /** Page url to share */ url?: string; label: string; labelTemplate: string; filename?: string; /** true if the browser runs on Android */ isAndroid: boolean; /** true if the browser runs on iOS */ isIos: boolean; /** true if the browser runs on a desktop computer */ isDesktop: boolean; /** true if the browser supports native share */ isNative: boolean; dropdownId: string; /** * Object with share urls like WhatsApp, Telegram, instagram etc used if the native share is not available * Only used if the browser has not an native share support like on android and iOS * */ shareItems: ShareItem[]; dropdownDirection: "up" | "down" | "start" | "end"; dropdownAlignment: "end" | "start" | "auto"; labelFacebook: string; labelTwitter: string; labelPinterest: string; labelWhatsapp: string; labelTelegram: string; labelEmail: string; labelDownload: string; labelClipboard: string; // Methods shareOnService: Bs5ShareComponent["shareOnService"]; share: Bs5ShareComponent["share"]; getFilename: Bs5ShareComponent["getFilename"]; } export interface NavigatorShareParam { url: string; text: string; title: string; } declare global { // tslint:disable: interface-name interface Navigator { share: (data?: ShareData) => Promise<void>; } } /** * Component to share the a link * Similar projects which are can share stuff: * * https://github.com/nimiq/web-share-shim * * http://webintents.org/ * * http://chriswren.github.io/native-social-interactions/ * * https://github.com/dimsemenov/PhotoSwipe/blob/master/src/js/ui/photoswipe-ui-default.js * */ export class Bs5ShareComponent extends Component { public static tagName = "bs5-share"; public _debug = false; static get observedAttributes(): string[] { return [ "type", "title", "text", "url", "media-url", "filename", "label", "dropdown-direction", "dropdown-alignment", "label-facebook", "label-twitter", "label-pinterest", "label-whatsapp", "label-telegram", "label-email", "label-download", "label-clipboard", ]; } protected dropdown?: Dropdown; // Count of Bs5ShareComponent components static count = 0; public scope: Scope; constructor() { super(); this.scope = this.getScopeDefaults(); // this.debug("constructor", this.scope); Bs5ShareComponent.count++; this.onExternalOpenEvent = this.onExternalOpenEvent.bind(this); this.onExternalCloseEvent = this.onExternalCloseEvent.bind(this); } public getFilename(item: ShareItem) { if (item.filename) { return item.filename; } const url = this.getMediaUrlForShare(); const filename = url.split("/").pop(); return filename; } protected getDefaultShareServices() { const newLine = "%0A"; const shareItems: ShareItem[] = [ { id: "facebook", label: this.scope.labelFacebook, // It is not possible to add a message on facebook sharer.php but with the Dialog API, see https://developers.facebook.com/docs/javascript/reference/FB.ui urlTemplate: "https://www.facebook.com/sharer/sharer.php?u={{url}}", mediaUrlTemplate: "https://www.facebook.com/sharer/sharer.php?u={{media_url}}", type: "popup", url: "", availableFor: ["page", "image", "video"], }, { id: "twitter", label: this.scope.labelTwitter, urlTemplate: "https://twitter.com/intent/tweet?text={{text}}&url={{url}}", mediaUrlTemplate: `https://twitter.com/intent/tweet?text={{text}}&url={{media_url}}${newLine}({{url}})`, url: "", availableFor: ["page", "image", "video"], }, { id: "pinterest", label: this.scope.labelPinterest, urlTemplate: "http://www.pinterest.com/pin/create/button/" + "?url={{url}}&media={{media_url}}&description={{text}}", type: "popup", url: "", availableFor: ["image", "video"], }, { id: "whatsapp", label: this.scope.labelWhatsapp, urlTemplate: `https://api.whatsapp.com/send?text={{text}}${newLine}${newLine}{{url}}`, mediaUrlTemplate: `https://api.whatsapp.com/send?text={{text}}${newLine}${newLine}{{media_url}}${newLine}({{url}})`, type: "popup", url: "", availableFor: ["page", "image", "video"], }, { id: "telegram", label: this.scope.labelTelegram, urlTemplate: `https://telegram.me/share/url?url={{url}}&text={{text}}`, mediaUrlTemplate: `https://telegram.me/share/url?url={{media_url}}&text={{text}}${newLine}({{url}})`, type: "popup", url: "", availableFor: ["page", "image", "video"], }, { id: "email", label: this.scope.labelEmail, urlTemplate: `mailto:?subject={{title}}&body={{text}}${newLine}${newLine}{{url}}`, mediaUrlTemplate: `mailto:?subject={{title}}&body={{text}}${newLine}${newLine}{{media_url}}${newLine}({{url}})`, type: "href", url: "", availableFor: ["page", "image", "video"], }, // { // id: "sms", // label: "SMS", // urlTemplate: "sms:?body={{text}}", // type: 'href', // url: "", // canPassUrl: false, // availableFor: ['page', 'image', 'video'], // }, { id: "download", label: this.scope.labelDownload, urlTemplate: "{{raw_media_url}}", type: "download", url: "", availableFor: ["image", "video"], filename: this.scope.filename, }, { id: "clipboard", label: this.scope.labelClipboard, urlTemplate: "{{url}}", mediaUrlTemplate: `{{media_url}}`, type: "clipboard", url: "", availableFor: ["page", "image", "video"], }, ]; return shareItems; } protected isIos() { return navigator.userAgent.match(/iPhone|iPad|iPod/i) !== null; } protected isAndroid() { return navigator.userAgent.match(/Android/i) !== null; } protected browserSupportsNativeShare() { return typeof navigator.share === "function"; } protected getScopeDefaults(): Scope { const scope: Scope = { type: "page", title: document.title, text: "Look at this! 👀🤩", url: undefined, label: "Share", labelTemplate, isAndroid: this.isAndroid(), isIos: this.isIos(), isDesktop: false, isNative: this.browserSupportsNativeShare(), dropdownId: "dropdownShare" + Bs5ShareComponent.count, shareItems: [], dropdownDirection: "down", dropdownAlignment: "auto", // Service labels labelFacebook: "Facebook", labelTwitter: "Twitter", labelPinterest: "Pinterest", labelWhatsapp: "Whatsapp", labelTelegram: "Telegram", labelEmail: "Email", labelDownload: "Download", labelClipboard: "Copy to clipboard", // Methods share: this.share, shareOnService: this.shareOnService, getFilename: this.getFilename, }; // on those two support "mobile deep links", so HTTP based fallback for all others. scope.isDesktop = !scope.isIos && !scope.isAndroid; return scope; } protected onExternalOpenEvent() { this.dropdown?.show(); } protected onExternalCloseEvent() { this.dropdown?.hide(); } protected connectedCallback() { super.connectedCallback(); this.init(Bs5ShareComponent.observedAttributes); this.addEventListeners(); } protected disconnectedCallback() { super.disconnectedCallback(); this.removeEventListeners(); } protected addEventListeners() { this.addEventListener("open", this.onExternalOpenEvent); this.addEventListener("btn-close", this.onExternalCloseEvent); } protected removeEventListeners() { this.removeEventListener("open", this.onExternalOpenEvent); this.removeEventListener("btn-close", this.onExternalOpenEvent); } protected getURLForShare() { if (this.scope.type === "page" && this.scope.url) { return getUrl(this.scope.url); } return window.location.href; } protected getMediaUrlForShare() { if (this.scope.type !== "page" && this.scope.url) { return getUrl(this.scope.url); } return ""; } protected getTextForShare() { return stripHtml(this.scope.text); } /** * Currently only used for email * @param appendUrl */ protected getTitleForShare() { return stripHtml(this.scope.title); } protected updateShareURLs() { for (const shareItem of this.scope.shareItems) { const url = this.getURLForShare(); const mediaUrl = this.getMediaUrlForShare(); const shareText = this.getTextForShare(); const shareTitle = this.getTitleForShare(); let urlTemplate = shareItem.urlTemplate; if (this.scope.type !== "page" && shareItem.mediaUrlTemplate) { urlTemplate = shareItem.mediaUrlTemplate; } const encode = shareItem.type === "clipboard" ? false : true; const shareURL = urlTemplate .replace("{{url}}", encode ? encodeURIComponent(url) : url) .replace( "{{media_url}}", encode ? encodeURIComponent(mediaUrl) : mediaUrl, ) .replace("{{raw_media_url}}", mediaUrl) .replace("{{text}}", encode ? encodeURIComponent(shareText) : shareText) .replace( "{{title}}", encode ? encodeURIComponent(shareTitle) : shareTitle, ); shareItem.available = shareItem.availableFor.includes(this.scope.type); shareItem.url = shareURL; } } protected initDropdown() { const dropDownButtonElement = this.querySelector( ".dropdown-toggle-share", ) as HTMLButtonElement | HTMLAnchorElement; if (!dropDownButtonElement) { console.warn( 'Element with selector ".dropdown-toggle-share" not found!', this, ); return; } this.dropdown = new Dropdown(dropDownButtonElement); } /** * New browser popup with the external site (e.g. Facebook) on you want to share your url * @param binding * @param event * @param controller * @param el */ public async shareOnService(item: ShareItem, event: Event) { this.dropdown?.hide(); if (item.type === "clipboard") { event.preventDefault(); event.stopPropagation(); await copyTextToClipboard(item.url); return false; } // We use the default browser anchor href logic for download and href if (item.type === "download") { return true; } event.preventDefault(); event.stopPropagation(); window.open( item.url, "Share", "scrollbars=yes,resizable=yes,toolbar=no," + "location=yes,width=550,height=420,top=100,left=" + (window.screen ? Math.round(screen.width / 2 - 275) : 100), ); return false; } public async share(event: Event): Promise<any> { this.debug("share", this.scope); event.preventDefault(); event.stopPropagation(); if (this.scope.isNative && !this.scope.isDesktop) { try { await navigator.share({ title: this.scope.title, text: `${this.scope.text}\r\n\r\n`, url: this.scope.url || window.location.href, }); } catch (error: any) { if (error.name === "AbortError") { // TODO show flash message // this.debug(error.message); return; } console.error(`Error ${error.name}: ${error.message}`, error); } } else { this.updateShareURLs(); return this.dropdown?.toggle(); } } protected async beforeBind() { await super.beforeBind(); // this.debug('beforeBind'); } protected async afterBind() { this.initDropdown(); this.debug("afterBind", this.scope); this.scope.shareItems = this.getDefaultShareServices(); await super.afterBind(); } protected requiredAttributes(): string[] { return []; } protected template(): ReturnType<TemplateFunction> { if (this && hasChildNodesTrim(this)) { this.scope.labelTemplate = this.innerHTML; } return template; } }