From 79c2c6e8cf13cdbd5658f1e5300ce71929c64f17 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Sat, 2 Mar 2024 17:11:41 +0100 Subject: [PATCH] feat: new updaterTransformerMethodsSymbol method #163 --- source/dom/customelement.mjs | 1775 +++++++++++++++++----------------- 1 file changed, 911 insertions(+), 864 deletions(-) diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs index 209700b64..4465099f8 100644 --- a/source/dom/customelement.mjs +++ b/source/dom/customelement.mjs @@ -5,63 +5,64 @@ * 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 {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, + isArray, + isFunction, + isIterable, + isObject, + isString, } from "../types/is.mjs"; -import { Observer } from "../types/observer.mjs"; -import { ProxyObserver } from "../types/proxyobserver.mjs"; +import {Observer} from "../types/observer.mjs"; +import {ProxyObserver} from "../types/proxyobserver.mjs"; import { - validateFunction, - validateInstance, - validateObject, - validateString, + validateFunction, + validateInstance, + validateObject, + validateString, } from "../types/validate.mjs"; -import { clone } from "../util/clone.mjs"; +import {clone} from "../util/clone.mjs"; import { - addAttributeToken, - getLinkedObjects, - hasObjectLink, + addAttributeToken, + getLinkedObjects, + hasObjectLink, } from "./attributes.mjs"; import { - ATTRIBUTE_DISABLED, - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_OPTIONS, - ATTRIBUTE_INIT_CALLBACK, - ATTRIBUTE_OPTIONS_SELECTOR, - ATTRIBUTE_SCRIPT_HOST, - customElementUpdaterLinkSymbol, - initControlCallbackName, + 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 {findDocumentTemplate, Template} from "./template.mjs"; +import {addObjectWithUpdaterToElement} from "./updater.mjs"; +import {instanceSymbol} from "../constants.mjs"; import { - getDocumentTranslations, - Translations, + 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"; +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, + CustomElement, + initMethodSymbol, + assembleMethodSymbol, + attributeObserverSymbol, + registerCustomElement, + getSlottedElements, + updaterTransformerMethodsSymbol }; /** @@ -75,7 +76,15 @@ const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol"); * @type {symbol} */ const assembleMethodSymbol = Symbol.for( - "@schukai/monster/dom/@@assembleMethodSymbol", + "@schukai/monster/dom/@@assembleMethodSymbol", +); + +/** + * @memberOf Monster.DOM + * @type {symbol} + */ +const updaterTransformerMethodsSymbol = Symbol.for( + "@schukai/monster/dom/@@updaterTransformerMethodsSymbol", ); /** @@ -84,7 +93,7 @@ const assembleMethodSymbol = Symbol.for( * @type {symbol} */ const attributeObserverSymbol = Symbol.for( - "@schukai/monster/dom/@@attributeObserver", + "@schukai/monster/dom/@@attributeObserver", ); /** @@ -92,7 +101,7 @@ const attributeObserverSymbol = Symbol.for( * @type {symbol} */ const attributeMutationObserverSymbol = Symbol( - "@schukai/monster/dom/@@mutationObserver", + "@schukai/monster/dom/@@mutationObserver", ); /** @@ -231,480 +240,517 @@ const scriptHostElementSymbol = Symbol("scriptHostElement"); * @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 = undefined) { - 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); - } - // 2024-01-21: remove this.defaults, otherwise it will overwrite - // the current settings that have already been made. - // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136 - extend(this[internalSymbol].getSubject()["options"], 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; - } - - this[updateCloneDataSymbol] = clone( - this[internalSymbol].getRealSubject()["options"], - ); - - addObjectWithUpdaterToElement.call( - this, - nodeList, - customElementUpdaterLinkSymbol, - this[updateCloneDataSymbol], - ); - - // Attach a mutation observer to observe changes to the attributes of the element - attachAttributeChangeMutationObserver.call(this); - - return this; - } - - /** - * You know what you are doing? This function is only for advanced users. - * The result is a clone of the internal data. - * - * @returns {*} - */ - getInternalUpdateCloneData() { - return clone(this[updateCloneDataSymbol]); - } - - /** - * 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); - } + /** + * 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 = undefined) { + 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); + } + // 2024-01-21: remove this.defaults, otherwise it will overwrite + // the current settings that have already been made. + // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136 + extend(this[internalSymbol].getSubject()["options"], 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 equipped with update for the dynamic change of the dom. + * The functions returned here can be used as pipe functions in the template. + * + * In the example, the function `my-transformer` is defined. In the template you can use it as follows: + * + * ```html + * <my-element data-monster-option-transformer="path:my-value | call:my-transformer"></my-element> + * ``` + * + * @example + * [updaterTransformerMethodsSymbol]() { + * return { + * "my-transformer": (value) => { + * switch (typeof Wert) { + * case "string": + * return value + "!"; + * case "Zahl": + * return value + 1; + * default: + * return value; + * } + * } + * }; + * }; + * + * @return {object} + * @since 2.43.0 + */ + [updaterTransformerMethodsSymbol]() { + return {}; + } + + /** + * 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; + } + + this[updateCloneDataSymbol] = clone( + this[internalSymbol].getRealSubject()["options"], + ); + + addObjectWithUpdaterToElement.call( + this, + nodeList, + customElementUpdaterLinkSymbol, + this[updateCloneDataSymbol], + ); + + // Attach a mutation observer to observe changes to the attributes of the element + attachAttributeChangeMutationObserver.call(this); + + return this; + } + + + /** + * You know what you are doing? This function is only for advanced users. + * The result is a clone of the internal data. + * + * @returns {*} + */ + getInternalUpdateCloneData() { + return clone(this[updateCloneDataSymbol]); + } + + /** + * 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); + } } /** @@ -713,50 +759,50 @@ class CustomElement extends HTMLElement { * @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`, - ); + 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`, + ); } /** @@ -773,16 +819,16 @@ function callControlCallback(callBackFunctionName, ...args) { * @since 1.8.0 */ function initFromCallbackHost() { - // Set the default callback function name - let callBackFunctionName = initControlCallbackName; + // 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); - } + // 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); + // Call the callback function with the element as a parameter if it exists + callControlCallback.call(this, callBackFunctionName); } /** @@ -792,35 +838,35 @@ function initFromCallbackHost() { * @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()); - } + 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()); + } } /** @@ -830,19 +876,19 @@ function attachAttributeChangeMutationObserver() { * @return {boolean} */ function containChildNode(node) { - if (this.contains(node)) { - return true; - } + if (this.contains(node)) { + return true; + } - for (const [, e] of Object.entries(this.childNodes)) { - if (e.contains(node)) { - return true; - } + for (const [, e] of Object.entries(this.childNodes)) { + if (e.contains(node)) { + return true; + } - containChildNode.call(e, node); - } + containChildNode.call(e, node); + } - return false; + return false; } /** @@ -852,89 +898,89 @@ function containChildNode(node) { * @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); - } - }; + 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); + } + }; } /** @@ -943,37 +989,37 @@ function initOptionObserver() { * @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; + 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; } /** @@ -981,21 +1027,21 @@ function getOptionsFromScriptTag() { * @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 {}; + 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 {}; } /** @@ -1007,25 +1053,26 @@ function getOptionsFromAttributes() { * @throws {error} Throws an error if the JSON data is not valid. */ 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); + 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); } /** @@ -1033,21 +1080,21 @@ function parseOptionsJSON(data) { * @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; + 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; } /** @@ -1060,49 +1107,49 @@ function initHtmlContent() { * @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; + 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; } /** @@ -1115,35 +1162,35 @@ function initCSSStylesheet() { * @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; + 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; } /** @@ -1158,20 +1205,20 @@ function initShadowRoot() { * @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."); - } - - const tag = element?.getTag(); - if (!isString(tag) || tag === "") { - throw new Error("tag is not set."); - } - - if (customElements.get(tag) !== undefined) { - return; - } - - customElements.define(tag, element); + validateFunction(element); + const customElements = getGlobalObject("customElements"); + if (customElements === undefined) { + throw new Error("customElements is not supported."); + } + + const tag = element?.getTag(); + if (!isString(tag) || tag === "") { + throw new Error("tag is not set."); + } + + if (customElements.get(tag) !== undefined) { + return; + } + + customElements.define(tag, element); } -- GitLab