@ribajs/bs5
Version:
Bootstrap 5 module for Riba.js
456 lines (405 loc) • 12.9 kB
text/typescript
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;
}
}