UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

552 lines (478 loc) 14.4 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { instanceSymbol } from "../../constants.mjs"; import { addAttributeToken, removeAttributeToken, } from "../../dom/attributes.mjs"; import { ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { CustomElement } from "../../dom/customelement.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { getDocument } from "../../dom/util.mjs"; import { STYLE_DISPLAY_MODE_BLOCK } from "../form/constants.mjs"; import { CopyStyleSheet } from "./stylesheet/copy.mjs"; import { fireCustomEvent } from "../../dom/events.mjs"; import { positionPopper } from "../form/util/floating-ui.mjs"; import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; export { Copy }; /** * @private * @type {symbol} */ const timerCallbackSymbol = Symbol("timerCallback"); /** * @private * @type {symbol} */ const timerDelaySymbol = Symbol("timerDelay"); /** * @private * @type {symbol} */ export const controlElementSymbol = Symbol("copyElement"); /** * @private * @type {symbol} */ export const popperElementSymbol = Symbol("popperElement"); /** * @private * @type {symbol} */ export const copyButtonElementSymbol = Symbol("copyButtonElement"); /** * local symbol * @private * @type {symbol} */ const closeEventHandler = Symbol("closeEventHandler"); /** * local symbol * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * A Copy Component * * @fragments /fragments/components/content/copy/ * * @example /examples/components/content/copy-simple Copy * * @since 3.77.0 * @copyright Volker Schukai * @summary A beautiful Copy that can make your life easier and also looks good. */ class Copy extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/content/copy@@instance"); } /** * * @return {Components.Content.Copy * @fires monster-copy-clicked This event is fired when the copy button is clicked. * @fires monster-copy-success This event is fired when the copy action is successful. * @fires monster-copy-error This event is fired when the copy action fails. */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); return this; } /** * This method is called when the element is connected to the dom. * * @return {void} */ connectedCallback() { super.connectedCallback(); const document = getDocument(); for (const [, type] of Object.entries(["click", "touch"])) { // close on outside ui-events document.addEventListener(type, this[closeEventHandler]); } updatePopper.call(this); attachResizeObserver.call(this); } /** * This method is called when the element is disconnected from the dom. * * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); // close on outside ui-events for (const [, type] of Object.entries(["click", "touch"])) { document.removeEventListener(type, this[closeEventHandler]); } disconnectResizeObserver.call(this); } /** * To set the options via the HTML Tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {Object} actions Callbacks * @property {string} actions.click="throw Error" Callback when clicked * @property {Object} features Features * @property {boolean} features.stripTags=true Strip tags from the copied text * @property {boolean} features.preventOpenEventSent=false Prevent open event from being sent * @property {Object} popper Popper configuration * @property {string} popper.placement="top" Popper placement * @property {string[]} popper.middleware=["autoPlacement", "shift", "offset:15", "arrow"] Popper middleware * @property {boolean} disabled=false Disabled state */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, disabled: false, features: { stripTags: true, preventOpenEventSent: false, }, popper: { placement: "top", middleware: ["autoPlacement", "offset:-1", "arrow"], }, }); } /** * @return {string} */ static getTag() { return "monster-copy"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [CopyStyleSheet]; } /** * With this method, you can show the popper. * * @return {Copy} */ showDialog() { if (this[timerDelaySymbol] instanceof DeadMansSwitch) { try { this[timerDelaySymbol].defuse(); } catch (e) {} } this[timerDelaySymbol] = new DeadMansSwitch(500, () => { show.call(this); }); return this; } /** * With this method, you can hide the popper. * * @return {Copy} */ hideDialog() { if (this[timerDelaySymbol] instanceof DeadMansSwitch) { try { this[timerDelaySymbol].defuse(); } catch (e) {} } hide.call(this); return this; } /** * With this method, you can toggle the popper. * * @return {Copy} */ toggleDialog() { if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { this.hideDialog(); } else { this.showDialog(); } return this; } } /** * @private */ function attachResizeObserver() { // against flickering this[resizeObserverSymbol] = new ResizeObserver((entries) => { if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { try { this[timerCallbackSymbol].touch(); return; } catch (e) { delete this[timerCallbackSymbol]; } } this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { updatePopper.call(this); }); }); requestAnimationFrame(() => { let parent = this.parentNode; while (!(parent instanceof HTMLElement) && parent !== null) { parent = parent.parentNode; } if (parent instanceof HTMLElement) { this[resizeObserverSymbol].observe(parent); } }); } /** * @private */ function disconnectResizeObserver() { if (this[resizeObserverSymbol] instanceof ResizeObserver) { this[resizeObserverSymbol].disconnect(); } } /** * @private */ function hide() { const self = this; fireCustomEvent(self, "monster-popper-hide", { self, }); self[popperElementSymbol].style.display = "none"; removeAttributeToken(self[controlElementSymbol], "class", "open"); setTimeout(() => { fireCustomEvent(self, "monster-popper-hidden", { self, }); }, 0); } /** * @private */ function show() { const self = this; if (self.getOption("disabled", false) === true) { return; } if (self[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { return; } fireCustomEvent(self, "monster-popper-open", { self, }); self[popperElementSymbol].style.visibility = "hidden"; self[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; addAttributeToken(self[controlElementSymbol], "class", "open"); updatePopper.call(self); setTimeout(() => { fireCustomEvent(self, "monster-popper-opened", { self, }); }, 0); } /** * @private */ function updatePopper() { if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { return; } if (this.getOption("disabled", false) === true) { return; } positionPopper.call( this, this[controlElementSymbol], this[popperElementSymbol], this.getOption("popper", {}), ); } /** * @private * @return {initEventHandler} */ function initEventHandler() { const self = this; this[closeEventHandler] = (event) => { const path = event.composedPath(); for (const [, element] of Object.entries(path)) { if (element === this) { return; } } hide.call(this); }; const type = "click"; this[controlElementSymbol].addEventListener("mouseenter", (event) => { if (this.getOption("features.preventOpenEventSent") === true) { event.preventDefault(); } this.showDialog(); }); this[controlElementSymbol].addEventListener("mouseleave", (event) => { if (this.getOption("features.preventOpenEventSent") === true) { event.preventDefault(); } this.hideDialog(); }); this[controlElementSymbol].addEventListener("focus", (event) => { if (this.getOption("features.preventOpenEventSent") === true) { event.preventDefault(); } this.showDialog(); }); this[controlElementSymbol].addEventListener("blur", (event) => { if (this.getOption("features.preventOpenEventSent") === true) { event.preventDefault(); } this.hideDialog(); }); this[copyButtonElementSymbol].addEventListener(type, function (event) { fireCustomEvent(self, "monster-copy-clicked", { element: self, }); const text = getSlottedCopyContent.call(self); navigator.clipboard .writeText(text) .then(function () { self[copyButtonElementSymbol] .querySelector("use") .setAttribute("href", "#copy-success"); setTimeout(() => { self[copyButtonElementSymbol] .querySelector("use") .setAttribute("href", "#copy"); }, 2000); fireCustomEvent(self, "monster-copy-success", { element: self, }); }) .catch(function (e) { self[copyButtonElementSymbol] .querySelector("use") .setAttribute("href", "#copy-error"); setTimeout(() => { self[copyButtonElementSymbol] .querySelector("use") .setAttribute("href", "#copy"); }, 2000); fireCustomEvent(self, "monster-copy-error", { element: self, }); addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, "" + e); }); }); return this; } /** * @private * @returns {Set<any>|string} */ function getSlottedCopyContent() { const self = this; const result = new Set(); if (!(this.shadowRoot instanceof ShadowRoot)) { return result; } const slots = this.shadowRoot.querySelectorAll("slot"); let text = ""; for (const [, slot] of Object.entries(slots)) { slot.assignedNodes().forEach(function (node) { if ( node instanceof HTMLElement || node instanceof SVGElement || node instanceof MathMLElement ) { if (self.getOption("features.stripTags")) { text += node.textContent.trim(); } else { text += node.outerHTML.trim(); } } else { text += node.textContent.trim(); } }); } return text; } /** * @private * @return {void} */ function initControlReferences() { this[controlElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="control"]`, ); this[copyButtonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="button"]`, ); this[popperElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="popper"]`, ); } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <slot></slot> <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1"> <div data-monster-role="arrow"></div> <button data-monster-role="button" part="button"> <svg data-monster-role="icon-map"> <defs> <g id="copy"> <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/> <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/> </g> <g id="copy-success"> <path fill-rule="evenodd" d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0"/> <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/> <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/> </g> <g id="copy-error"> <path fill-rule="evenodd" d="M6.146 7.146a.5.5 0 0 1 .708 0L8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 0 1 0-.708"/> <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/> <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/> </g> </defs> </svg> <svg data-monster-role="icon" xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 16 16"> <use href="#copy"></use> </svg> </button> </div> </div>`; } registerCustomElement(Copy);