/** * Copyright schukai GmbH and contributors 2023. All Rights Reserved. * Node module: @schukai/monster * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ import { findElementWithIdUpwards } from "./util.mjs"; import { internalSymbol } from "../constants.mjs"; import { extend } from "../data/extend.mjs"; import { Pathfinder } from "../data/pathfinder.mjs"; import { Formatter } from "../text/formatter.mjs"; import { parseDataURL } from "../types/dataurl.mjs"; import { getGlobalObject } from "../types/global.mjs"; import { isArray, isFunction, isIterable, isObject, isString, } from "../types/is.mjs"; import { Observer } from "../types/observer.mjs"; import { ProxyObserver } from "../types/proxyobserver.mjs"; import { validateFunction, validateInstance, validateObject, validateString, } from "../types/validate.mjs"; import { clone } from "../util/clone.mjs"; import { addAttributeToken, getLinkedObjects, hasObjectLink, } from "./attributes.mjs"; import { ATTRIBUTE_DISABLED, ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_OPTIONS, ATTRIBUTE_INIT_CALLBACK, ATTRIBUTE_OPTIONS_SELECTOR, ATTRIBUTE_SCRIPT_HOST, customElementUpdaterLinkSymbol, initControlCallbackName, } from "./constants.mjs"; import { findDocumentTemplate, Template } from "./template.mjs"; import { addObjectWithUpdaterToElement } from "./updater.mjs"; import { instanceSymbol } from "../constants.mjs"; import { getDocumentTranslations, Translations, } from "../i18n/translations.mjs"; import { getSlottedElements } from "./slotted.mjs"; import { initOptionsFromAttributes } from "./util/init-options-from-attributes.mjs"; import { setOptionFromAttribute } from "./util/set-option-from-attribute.mjs"; export { CustomElement, initMethodSymbol, assembleMethodSymbol, attributeObserverSymbol, registerCustomElement, getSlottedElements, }; /** * @memberOf Monster.DOM * @type {symbol} */ const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol"); /** * @memberOf Monster.DOM * @type {symbol} */ const assembleMethodSymbol = Symbol.for( "@schukai/monster/dom/@@assembleMethodSymbol", ); /** * this symbol holds the attribute observer callbacks. The key is the attribute name. * @memberOf Monster.DOM * @type {symbol} */ const attributeObserverSymbol = Symbol.for( "@schukai/monster/dom/@@attributeObserver", ); /** * @private * @type {symbol} */ const attributeMutationObserverSymbol = Symbol( "@schukai/monster/dom/@@mutationObserver", ); /** * @private * @type {symbol} */ const scriptHostElementSymbol = Symbol("scriptHostElement"); /** * HTMLElement * @external HTMLElement * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement * * @startuml customelement-sequencediagram.png * skinparam monochrome true * skinparam shadowing false * * autonumber * * Script -> DOM: element = document.createElement('my-element') * DOM -> CustomElement: constructor() * CustomElement -> CustomElement: [initMethodSymbol]() * * CustomElement --> DOM: Element * DOM --> Script : element * * * Script -> DOM: document.querySelector('body').append(element) * * DOM -> CustomElement : connectedCallback() * * note right CustomElement: is only called at\nthe first connection * CustomElement -> CustomElement : [assembleMethodSymbol]() * * ... ... * * autonumber * * Script -> DOM: document.querySelector('monster-confirm-button').parentNode.removeChild(element) * DOM -> CustomElement: disconnectedCallback() * * * @enduml * * @startuml customelement-class.png * skinparam monochrome true * skinparam shadowing false * HTMLElement <|-- CustomElement * @enduml */ /** * The `CustomElement` class provides a way to define a new HTML element using the power of Custom Elements. * * **IMPORTANT:** After defining a `CustomElement`, the `registerCustomElement` method must be called with the new class name * to make the tag defined via the `getTag` method known to the DOM. * * You can create an instance of the object via the `document.createElement()` function. * * ## Interaction * * <img src="./images/customelement-sequencediagram.png"> * * ## Styling * * To display custom elements optimally, the `:defined` pseudo-class can be used. To prevent custom elements from being displayed and flickering until the control is registered, * it is recommended to create a CSS directive. * * In the simplest case, you can simply hide the control: * * ```html * <style> * my-custom-element:not(:defined) { * display: none; * } * * my-custom-element:defined { * display: flex; * } * </style> * ``` * * Alternatively, you can display a loader: * * ```css * my-custom-element:not(:defined) { * display: flex; * box-shadow: 0 4px 10px 0 rgba(33, 33, 33, 0.15); * border-radius: 4px; * height: 200px; * position: relative; * overflow: hidden; * } * * my-custom-element:not(:defined)::before { * content: ''; * display: block; * position: absolute; * left: -150px; * top: 0; * height: 100%; * width: 150px; * background: linear-gradient(to right, transparent 0%, #E8E8E8 50%, transparent 100%); * animation: load 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite; * } * * @keyframes load { * from { * left: -150px; * } * to { * left: 100%; * } * } * * my-custom-element:defined { * display: flex; * } * ``` * * More information about Custom Elements can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). * And in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements). * * @externalExample ../../example/dom/theme.mjs * @license AGPLv3 * @since 1.7.0 * @copyright schukai GmbH * @memberOf Monster.DOM * @extends external:HTMLElement * @summary A base class for HTML5 custom controls. */ class CustomElement extends HTMLElement { /** * A new object is created. First the `initOptions` method is called. Here the * options can be defined in derived classes. Subsequently, the shadowRoot is initialized. * * IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via <code>document.createElement()</code>. * * @throws {Error} the options attribute does not contain a valid json definition. * @since 1.7.0 */ constructor() { super(); this[attributeObserverSymbol] = {}; this[internalSymbol] = new ProxyObserver({ options: initOptionsFromAttributes(this, extend({}, this.defaults)), }); this[initMethodSymbol](); initOptionObserver.call(this); this[scriptHostElementSymbol] = []; } /** * This method is called by the `instanceof` operator. * @returns {symbol} * @since 2.1.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/dom/custom-element@@instance"); } /** * This method determines which attributes are to be * monitored by `attributeChangedCallback()`. Unfortunately, this method is static. * Therefore, the `observedAttributes` property cannot be changed during runtime. * * @return {string[]} * @since 1.15.0 */ static get observedAttributes() { return []; } /** * * @param attribute * @param callback * @returns {Monster.DOM.CustomElement} */ addAttributeObserver(attribute, callback) { validateFunction(callback); this[attributeObserverSymbol][attribute] = callback; return this; } /** * * @param attribute * @returns {Monster.DOM.CustomElement} */ removeAttributeObserver(attribute) { delete this[attributeObserverSymbol][attribute]; return this; } /** * The `defaults` property defines the default values for a control. If you want to override these, * you can use various methods, which are described in the documentation available at * {@link https://monsterjs.orgendocconfigurate-a-monster-control}. * * The individual configuration values are listed below: * * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow), * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements). * * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template). * * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot). * * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form. * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it. * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling. * @property {Object} templates Specifies the templates used by the control. * @property {string} templates.main=undefined Specifies the main template used by the control. * @property {Object} templateMapping Specifies the mapping of templates. * @since 1.8.0 */ get defaults() { return { disabled: false, shadowMode: "open", delegatesFocus: true, templates: { main: undefined, }, templateMapping: {}, }; } /** * This method updates the labels of the element. * The labels are defined in the options object. * The key of the label is used to retrieve the translation from the document. * If the translation is different from the label, the label is updated. * * Before you can use this method, you must have loaded the translations. * * @returns {Monster.DOM.CustomElement} * @throws {Error} Cannot find element with translations. Add a translations object to the document. */ updateI18n() { const translations = getDocumentTranslations(); if (!translations) { return this; } const labels = this.getOption("labels"); if (!(isObject(labels) || isIterable(labels))) { return this; } for (const key in labels) { const def = labels[key]; if (isString(def)) { const text = translations.getText(key, def); if (text !== def) { this.setOption(`labels.${key}`, text); } continue; } else if (isObject(def)) { for (const k in def) { const d = def[k]; const text = translations.getPluralRuleText(key, k, d); if (!isString(text)) { throw new Error("Invalid labels definition"); } if (text !== d) { this.setOption(`labels.${key}.${k}`, text); } } continue; } throw new Error("Invalid labels definition"); } return this; } /** * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten * by the derived class. * * Note that there is no check on the name of the tag in this class. It is the responsibility of * the developer to assign an appropriate tag name. If the name is not valid, the * `registerCustomElement()` method will issue an error. * * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name * @throws {Error} This method must be overridden by the derived class. * @return {string} The tag name associated with the custom element. * @since 1.7.0 */ static getTag() { throw new Error( "The method `getTag()` must be overridden by the derived class.", ); } /** * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element. * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour. * * If `undefined` is returned, then the shadow root does not receive a stylesheet. * * Example usage: * * ```js * static getCSSStyleSheet() { * const sheet = new CSSStyleSheet(); * sheet.replaceSync("p { color: red; }"); * return sheet; * } * ``` * * If the environment does not support the `CSSStyleSheet` constructor, * you can use the following workaround to create the stylesheet: * * ```js * const doc = document.implementation.createHTMLDocument('title'); * let style = doc.createElement("style"); * style.innerHTML = "p { color: red; }"; * style.appendChild(document.createTextNode("")); * doc.head.appendChild(style); * return doc.styleSheets[0]; * ``` * * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied. */ static getCSSStyleSheet() { return undefined; } /** * attach a new observer * * @param {Observer} observer * @returns {CustomElement} */ attachObserver(observer) { this[internalSymbol].attachObserver(observer); return this; } /** * detach a observer * * @param {Observer} observer * @returns {CustomElement} */ detachObserver(observer) { this[internalSymbol].detachObserver(observer); return this; } /** * @param {Observer} observer * @returns {ProxyObserver} */ containsObserver(observer) { return this[internalSymbol].containsObserver(observer); } /** * nested options can be specified by path `a.b.c` * * @param {string} path * @param {*} defaultValue * @return {*} * @since 1.10.0 */ getOption(path, defaultValue) { let value; try { value = new Pathfinder( this[internalSymbol].getRealSubject()["options"], ).getVia(path); } catch (e) {} if (value === undefined) return defaultValue; return value; } /** * Set option and inform elements * * @param {string} path * @param {*} value * @return {CustomElement} * @since 1.14.0 */ setOption(path, value) { new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia( path, value, ); return this; } /** * @since 1.15.0 * @param {string|object} options * @return {CustomElement} */ setOptions(options) { if (isString(options)) { options = parseOptionsJSON.call(this, options); }; extend( this[internalSymbol].getSubject()["options"], this.defaults, options, ); return this; } /** * Is called once via the constructor * * @return {CustomElement} * @since 1.8.0 */ [initMethodSymbol]() { return this; } /** * This method is called once when the object is included in the DOM for the first time. It performs the following actions: * 1. Extracts the options from the attributes and the script tag of the element and sets them. * 2. Initializes the shadow root and its CSS stylesheet (if specified). * 3. Initializes the HTML content of the element. * 4. Initializes the custom elements inside the shadow root and the slotted elements. * 5. Attaches a mutation observer to observe changes to the attributes of the element. * * @return {CustomElement} - The updated custom element. * @since 1.8.0 */ [assembleMethodSymbol]() {; let elements; let nodeList; // Extract options from attributes and set them const AttributeOptions = getOptionsFromAttributes.call(this); if ( isObject(AttributeOptions) && Object.keys(AttributeOptions).length > 0 ) { this.setOptions(AttributeOptions); } // Extract options from script tag and set them const ScriptOptions = getOptionsFromScriptTag.call(this); if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) { this.setOptions(ScriptOptions); } // Initialize the shadow root and its CSS stylesheet if (this.getOption("shadowMode", false) !== false) { try { initShadowRoot.call(this); elements = this.shadowRoot.childNodes; } catch (e) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString()); } try { initCSSStylesheet.call(this); } catch (e) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString()); } } // If the elements are not found inside the shadow root, initialize the HTML content of the element if (!(elements instanceof NodeList)) { initHtmlContent.call(this); elements = this.childNodes; } // Initialize the custom elements inside the shadow root and the slotted elements initFromCallbackHost.call(this); try { nodeList = new Set([...elements, ...getSlottedElements.call(this)]); } catch (e) { nodeList = elements; } addObjectWithUpdaterToElement.call( this, nodeList, customElementUpdaterLinkSymbol, clone(this[internalSymbol].getRealSubject()["options"]), ); // Attach a mutation observer to observe changes to the attributes of the element attachAttributeChangeMutationObserver.call(this); return this; } /** * This method is called every time the element is inserted into the DOM. It checks if the custom element * has already been initialized and if not, calls the assembleMethod to initialize it. * * @return {void} * @since 1.7.0 * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback */ connectedCallback() {; // Check if the object has already been initialized if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) { // If not, call the assembleMethod to initialize the object this[assembleMethodSymbol](); } } /** * Called every time the element is removed from the DOM. Useful for running clean up code. * * @return {void} * @since 1.7.0 */ disconnectedCallback() {} /** * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)). * * @return {void} * @since 1.7.0 */ adoptedCallback() {} /** * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes * property will receive this callback. * * @param {string} attrName * @param {string} oldVal * @param {string} newVal * @return {void} * @since 1.15.0 */ attributeChangedCallback(attrName, oldVal, newVal) {; if (attrName.startsWith("data-monster-option-")) { setOptionFromAttribute( this, attrName, this[internalSymbol].getSubject()["options"], ); } const callback = this[attributeObserverSymbol]?.[attrName]; if (isFunction(callback)) { try { callback.call(this, newVal, oldVal); } catch (e) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString()); } } } /** * * @param {Node} node * @return {boolean} * @throws {TypeError} value is not an instance of * @since 1.19.0 */ hasNode(node) {; if (containChildNode.call(this, validateInstance(node, Node))) { return true; } if (!(this.shadowRoot instanceof ShadowRoot)) { return false; } return containChildNode.call(this.shadowRoot, node); } /** * Calls a callback function if it exists. * * @param {string} name * @param {*} args * @returns {*} */ callCallback(name, args) {; return callControlCallback.call(this, name, ...args); } } /** * @param {string} callBackFunctionName * @param {*} args * @return {any} */ function callControlCallback(callBackFunctionName, ...args) {; if (!isString(callBackFunctionName) || callBackFunctionName === "") { return; } if (callBackFunctionName in this) { return this[callBackFunctionName](this, ...args); } if (!this.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) { return; } if (this[scriptHostElementSymbol].length === 0) { const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST); if (!targetId) { return; } const list = targetId.split(","); for (const id of list) { const host = findElementWithIdUpwards(this, targetId); if (!(host instanceof HTMLElement)) { continue; } this[scriptHostElementSymbol].push(host); } } for (const host of this[scriptHostElementSymbol]) { if (callBackFunctionName in host) { try { return host[callBackFunctionName](this, ...args); } catch (e) { addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString()); } } } addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, `callback ${callBackFunctionName} not found`, ); } /** * Initializes the custom element based on the provided callback function. * * This function is called when the element is attached to the DOM. It checks if the * `data-monster-option-callback` attribute is set, and if not, the default callback * `initCustomControlCallback` is called. The callback function is searched for in this * element and in the host element. If the callback is found, it is called with the element * as a parameter. * * @this CustomElement * @see https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#providing_a_construction_callback * @since 1.8.0 */ function initFromCallbackHost() {; // Set the default callback function name let callBackFunctionName = initControlCallbackName; // If the `data-monster-option-callback` attribute is set, use its value as the callback function name if (this.hasAttribute(ATTRIBUTE_INIT_CALLBACK)) { callBackFunctionName = this.getAttribute(ATTRIBUTE_INIT_CALLBACK); } // Call the callback function with the element as a parameter if it exists callControlCallback.call(this, callBackFunctionName); } /** * This method is called when the element is first created. * * @private * @this CustomElement */ function attachAttributeChangeMutationObserver() { const self = this; if (typeof self[attributeMutationObserverSymbol] !== "undefined") { return; } self[attributeMutationObserverSymbol] = new MutationObserver(function ( mutations, observer, ) { for (const mutation of mutations) { if (mutation.type === "attributes") { self.attributeChangedCallback( mutation.attributeName, mutation.oldValue, mutation.target.getAttribute(mutation.attributeName), ); } } }); try { self[attributeMutationObserverSymbol].observe(self, { attributes: true, attributeOldValue: true, }); } catch (e) { addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.toString()); } } /** * @this CustomElement * @private * @param {Node} node * @return {boolean} */ function containChildNode(node) {; if (this.contains(node)) { return true; } for (const [, e] of Object.entries(this.childNodes)) { if (e.contains(node)) { return true; } containChildNode.call(e, node); } return false; } /** * @license AGPLv3 * @since 1.15.0 * @private * @this CustomElement */ function initOptionObserver() { const self = this; let lastDisabledValue = undefined; self.attachObserver( new Observer(function () { const flag = self.getOption("disabled"); if (flag === lastDisabledValue) { return; } lastDisabledValue = flag; if (!(self.shadowRoot instanceof ShadowRoot)) { return; } const query = "button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]"; const elements = self.shadowRoot.querySelectorAll(query); let nodeList; try { nodeList = new Set([ ...elements, ...getSlottedElements.call(self, query), ]); } catch (e) { nodeList = elements; } for (const element of [...nodeList]) { if (flag === true) { element.setAttribute(ATTRIBUTE_DISABLED, ""); } else { element.removeAttribute(ATTRIBUTE_DISABLED); } } }), ); self.attachObserver( new Observer(function () { // not initialised if (!hasObjectLink(self, customElementUpdaterLinkSymbol)) { return; } // inform every element const updaters = getLinkedObjects(self, customElementUpdaterLinkSymbol); for (const list of updaters) { for (const updater of list) { const d = clone(self[internalSymbol].getRealSubject()["options"]); Object.assign(updater.getSubject(), d); } } }), ); // disabled self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => { if (self.hasAttribute(ATTRIBUTE_DISABLED)) { self.setOption(ATTRIBUTE_DISABLED, true); } else { self.setOption(ATTRIBUTE_DISABLED, undefined); } }; // data-monster-options self[attributeObserverSymbol][ATTRIBUTE_OPTIONS] = () => { const options = getOptionsFromAttributes.call(self); if (isObject(options) && Object.keys(options).length > 0) { self.setOptions(options); } }; // data-monster-options-selector self[attributeObserverSymbol][ATTRIBUTE_OPTIONS_SELECTOR] = () => { const options = getOptionsFromScriptTag.call(self); if (isObject(options) && Object.keys(options).length > 0) { self.setOptions(options); } }; } /** * @private * @return {object} * @throws {TypeError} value is not a object */ function getOptionsFromScriptTag() {; if (!this.hasAttribute(ATTRIBUTE_OPTIONS_SELECTOR)) { return {}; } const node = document.querySelector( this.getAttribute(ATTRIBUTE_OPTIONS_SELECTOR), ); if (!(node instanceof HTMLScriptElement)) { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, `the selector ${ATTRIBUTE_OPTIONS_SELECTOR} for options was specified (${this.getAttribute( ATTRIBUTE_OPTIONS_SELECTOR, )}) but not found.`, ); return {}; } let obj = {}; try { obj = parseOptionsJSON.call(this, node.textContent.trim()); } catch (e) { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, `when analyzing the configuration from the script tag there was an error. ${e}`, ); } return obj; } /** * @private * @return {object} */ function getOptionsFromAttributes() {; if (this.hasAttribute(ATTRIBUTE_OPTIONS)) { try { return parseOptionsJSON.call(this, this.getAttribute(ATTRIBUTE_OPTIONS)); } catch (e) { addAttributeToken( this, ATTRIBUTE_ERRORMESSAGE, `the options attribute ${ATTRIBUTE_OPTIONS} does not contain a valid json definition (actual: ${this.getAttribute( ATTRIBUTE_OPTIONS, )}).${e}`, ); } } return {}; } /** * @private * @param data * @return {Object} */ function parseOptionsJSON(data) { let obj = {}; if (!isString(data)) { return obj; } // the configuration can be specified as a data url. try { const dataUrl = parseDataURL(data); data = dataUrl.content; } catch (e) {} try { obj = JSON.parse(data); } catch (e) { throw e; } return validateObject(obj); } /** * @private * @return {initHtmlContent} */ function initHtmlContent() { try { const template = findDocumentTemplate(this.constructor.getTag()); this.appendChild(template.createDocumentFragment()); } catch (e) { let html = this.getOption("templates.main", ""); if (isString(html) && html.length > 0) { const mapping = this.getOption("templateMapping", {}); if (isObject(mapping)) { html = new Formatter(mapping).format(html); } this.innerHTML = html; } } return this; } /** * @private * @return {CustomElement} * @memberOf Monster.DOM * @this CustomElement * @license AGPLv3 * @since 1.16.0 * @throws {TypeError} value is not an instance of */ function initCSSStylesheet() {; if (!(this.shadowRoot instanceof ShadowRoot)) { return this; } const styleSheet = this.constructor.getCSSStyleSheet(); if (styleSheet instanceof CSSStyleSheet) { if (styleSheet.cssRules.length > 0) { this.shadowRoot.adoptedStyleSheets = [styleSheet]; } } else if (isArray(styleSheet)) { const assign = []; for (const s of styleSheet) { if (isString(s)) { const trimedStyleSheet = s.trim(); if (trimedStyleSheet !== "") { const style = document.createElement("style"); style.innerHTML = trimedStyleSheet; this.shadowRoot.prepend(style); } continue; } validateInstance(s, CSSStyleSheet); if (s.cssRules.length > 0) { assign.push(s); } } if (assign.length > 0) { this.shadowRoot.adoptedStyleSheets = assign; } } else if (isString(styleSheet)) { const trimedStyleSheet = styleSheet.trim(); if (trimedStyleSheet !== "") { const style = document.createElement("style"); style.innerHTML = styleSheet; this.shadowRoot.prepend(style); } } return this; } /** * @private * @return {CustomElement} * @throws {Error} html is not set. * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow * @memberOf Monster.DOM * @license AGPLv3 * @since 1.8.0 */ function initShadowRoot() { let template; let html; try { template = findDocumentTemplate(this.constructor.getTag()); } catch (e) { html = this.getOption("templates.main", ""); if (!isString(html) || html === undefined || html === "") { throw new Error("html is not set."); } } this.attachShadow({ mode: this.getOption("shadowMode", "open"), delegatesFocus: this.getOption("delegatesFocus", true), }); if (template instanceof Template) { this.shadowRoot.appendChild(template.createDocumentFragment()); return this; } const mapping = this.getOption("templateMapping", {}); if (isObject(mapping)) { html = new Formatter(mapping).format(html); } this.shadowRoot.innerHTML = html; return this; } /** * This method registers a new element. The string returned by `CustomElement.getTag()` is used as the tag. * * @param {CustomElement} element * @return {void} * @license AGPLv3 * @since 1.7.0 * @copyright schukai GmbH * @memberOf Monster.DOM * @throws {DOMException} Failed to execute 'define' on 'CustomElementRegistry': is not a valid custom element name */ function registerCustomElement(element) { validateFunction(element); const customElements = getGlobalObject("customElements"); if (customElements === undefined) { throw new Error("customElements is not supported."); } if (customElements.get(element.getTag()) !== undefined) { return; } customElements.define(element.getTag(), element); }