Skip to content
Snippets Groups Projects
Select Git revision
  • ef8ef78c5168be8cbd886118caf298c828338fb4
  • master default protected
  • 1.31
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
  • 4.28.0
  • 4.27.0
  • 4.26.0
  • 4.25.5
  • 4.25.4
  • 4.25.3
  • 4.25.2
  • 4.25.1
23 results

valid.mjs

Blame
  • customelement.mjs 31.97 KiB
    /**
     * Copyright © schukai GmbH 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 schukai GmbH.
     *
     * SPDX-License-Identifier: AGPL-3.0
     */
    
    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,
    } 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 } 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,
    	updaterTransformerMethodsSymbol,
    };
    
    /**
     * @type {symbol}
     */
    const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol");
    
    /**
     * @type {symbol}
     */
    const assembleMethodSymbol = Symbol.for(
    	"@schukai/monster/dom/@@assembleMethodSymbol",
    );
    
    /**
     * @type {symbol}
     */
    const updaterTransformerMethodsSymbol = Symbol.for(
    	"@schukai/monster/dom/@@updaterTransformerMethodsSymbol",
    );
    
    /**
     * this symbol holds the attribute observer callbacks. The key is the attribute name.
     * @type {symbol}
     */
    const attributeObserverSymbol = Symbol.for(
    	"@schukai/monster/dom/@@attributeObserver",
    );
    
    /**
     * @private
     * @type {symbol}
     */
    const attributeMutationObserverSymbol = Symbol(
    	"@schukai/monster/dom/@@mutationObserver",
    );
    
    /**
     * @private
     * @type {symbol}
     */
    const updateCloneDataSymbol = Symbol("@schukai/monster/dom/@@updateCloneData");
    
    /**
     * @private
     * @type {symbol}
     */
    const scriptHostElementSymbol = Symbol("scriptHostElement");
    
    /**
     * 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.
     *
     * ## 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).
     *
     * @license AGPLv3
     * @since 1.7.0
     * @copyright schukai GmbH
     * @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 option attribute does not contain a valid JSON definition.
    	 */
    	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.
    	 *
    	 * @return {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
    	 * @return {Monster.DOM.CustomElement}
    	 */
    	addAttributeObserver(attribute, callback) {
    		validateFunction(callback);
    		this[attributeObserverSymbol][attribute] = callback;
    		return this;
    	}
    
    	/**
    	 *
    	 * @param attribute
    	 * @return {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.
    	 * @property {Object} templateFormatter Specifies the formatter for the templates.
    	 * @property {Object} templateFormatter.marker Specifies the marker for the templates.
    	 * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates.
    	 * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates.
    	 * @property {Boolean} eventProcessing=false Specifies whether the control processes events.
    	 * @since 1.8.0
    	 */
    	get defaults() {
    		return {
    			disabled: false,
    			shadowMode: "open",
    			delegatesFocus: true,
    			templates: {
    				main: undefined,
    			},
    			templateMapping: {},
    			templateFormatter: {
    				marker: {
    					open: null,
    					close: null,
    				},
    			},
    
    			eventProcessing: false,
    		};
    	}
    
    	/**
    	 * This method updates the labels of the element.
    	 * The labels are defined in the option 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.
    	 *
    	 * @return {Monster.DOM.CustomElement}
    	 * @throws {Error}  Cannot find an element with translations. Add a translation object to the document.
    	 */
    	updateI18n() {
    		let translations;
    
    		try {
    			translations = getDocumentTranslations();
    		} catch (e) {
    			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString());
    			return this;
    		}
    
    		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
    	 * class MyElement extends CustomElement {
    	 *   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
    	 * @return {CustomElement}
    	 */
    	attachObserver(observer) {
    		this[internalSymbol].attachObserver(observer);
    		return this;
    	}
    
    	/**
    	 * detach a observer
    	 *
    	 * @param {Observer} observer
    	 * @return {CustomElement}
    	 */
    	detachObserver(observer) {
    		this[internalSymbol].detachObserver(observer);
    		return this;
    	}
    
    	/**
    	 * @param {Observer} observer
    	 * @return {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>
    	 * ```
    	 *
    	 * The function `my-transformer` is called with the value of `my-value` as a parameter.
    	 *
    	 * ```js
    	 * class MyElement extends CustomElement {
    	 * [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:
    	 *
    	 * <ol>
    	 * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li>
    	 * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li>
    	 * <li>Initializes the HTML content of the element.</li>
    	 * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li>
    	 * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li>
    	 *
    	 * @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"],
    		);
    
    		const cfg = {};
    		if (this.getOption("eventProcessing") === true) {
    			cfg.eventProcessing = true;
    		}
    
    		addObjectWithUpdaterToElement.call(
    			this,
    			nodeList,
    			customElementUpdaterLinkSymbol,
    			this[updateCloneDataSymbol],
    			cfg,
    		);
    
    		// 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.
    	 *
    	 * @return {*}
    	 */
    	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
    	 * @return {*}
    	 */
    	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 {};
    }
    
    /**
     * Parses the given JSON data and returns the parsed object.
     *
     * @private
     * @param {string} data The JSON data to be parsed.
     * @return {Object} The parsed object.
     * @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);
    }
    
    /**
     * @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}
     * @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
     * @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)) {
    		const formatter = new Formatter(mapping);
    		if (this.getOption("templateFormatter.marker.open") !== null) {
    			formatter.setMarker(
    				this.getOption("templateFormatter.marker.open"),
    				this.getOption("templateFormatter.marker.close"),
    			);
    		}
    		html = formatter.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
     * @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);
    }