Something went wrong on our end
Select Git revision
overlay.mjs
-
Volker Schukai authoredVolker Schukai authored
customelement.mjs 29.76 KiB
/**
* 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);
}