diff --git a/source/components/datatable/change-button.mjs b/source/components/datatable/change-button.mjs index c058e9db52c9f52162aefd0e68cab042764fa2b9..1d5fbb952eace8466aa488d67aed655e36ed6a13 100644 --- a/source/components/datatable/change-button.mjs +++ b/source/components/datatable/change-button.mjs @@ -5,8 +5,8 @@ import { instanceSymbol } from "../../constants.mjs"; import { diff } from "../../data/diff.mjs"; -import {addAttributeToken} from "../../dom/attributes.mjs"; -import {ATTRIBUTE_ERRORMESSAGE} from "../../dom/constants.mjs"; +import { addAttributeToken } from "../../dom/attributes.mjs"; +import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs"; import { assembleMethodSymbol, CustomElement, @@ -116,7 +116,7 @@ class ChangeButton extends CustomElement { * This method is responsible for assembling the component. */ [assembleMethodSymbol]() { - super[assembleMethodSymbol]();; + super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); @@ -213,7 +213,11 @@ function getIndex() { const ref = row.getAttribute("data-monster-insert-reference"); if (!ref) { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "reference is missing or empty"); + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + "reference is missing or empty", + ); return; } diff --git a/source/components/datatable/dataset.mjs b/source/components/datatable/dataset.mjs index 136c1864de4f56f49e2acf7b533e2fe4cc4322fd..05937e7830d256ec4a3359d8d73f07b0865fda10 100644 --- a/source/components/datatable/dataset.mjs +++ b/source/components/datatable/dataset.mjs @@ -136,7 +136,7 @@ class DataSet extends CustomElement { return "monster-dataset"; } - write() {; + write() { return new Promise((resolve, reject) => { if (!this[datasourceLinkedElementSymbol]) { diff --git a/source/components/datatable/filter.mjs b/source/components/datatable/filter.mjs index 744968b692782b20ceb10d9dc40d935021d0f092..508a8615d0e8b02d2bb1476cbdb8c9ba1b6b4424 100644 --- a/source/components/datatable/filter.mjs +++ b/source/components/datatable/filter.mjs @@ -815,7 +815,6 @@ function updateFilterTabs() { * @returns {Promise<*>} */ function doSearch({ showEffect } = { showEffect: true }) { - this.resetFailureMessage(); if (showEffect) { diff --git a/source/components/form/select.mjs b/source/components/form/select.mjs index 038e649f6556053528d3e575485b6d839f4fbf13..8c312d1f248404dd034c4571cdbda0cd8ea4c5fa 100644 --- a/source/components/form/select.mjs +++ b/source/components/form/select.mjs @@ -4,61 +4,62 @@ * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ -import {instanceSymbol} from "../../constants.mjs"; -import {internalSymbol} from "../../constants.mjs"; -import {buildMap} from "../../data/buildmap.mjs"; -import {DeadMansSwitch} from "../../util/deadmansswitch.mjs"; -import {positionPopper} from "./util/floating-ui.mjs"; +import { instanceSymbol } from "../../constants.mjs"; +import { internalSymbol } from "../../constants.mjs"; +import { buildMap } from "../../data/buildmap.mjs"; +import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; +import { positionPopper } from "./util/floating-ui.mjs"; import { - addAttributeToken, - findClosestByAttribute, - removeAttributeToken, + addAttributeToken, + findClosestByAttribute, + removeAttributeToken, } from "../../dom/attributes.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_PREFIX, - ATTRIBUTE_ROLE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_PREFIX, + ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; -import {CustomControl} from "../../dom/customcontrol.mjs"; +import { CustomControl } from "../../dom/customcontrol.mjs"; import { - assembleMethodSymbol, - getSlottedElements, - registerCustomElement, updaterTransformerMethodsSymbol, + assembleMethodSymbol, + getSlottedElements, + registerCustomElement, + updaterTransformerMethodsSymbol, } from "../../dom/customelement.mjs"; import { - findTargetElementFromEvent, - fireCustomEvent, - fireEvent, + findTargetElementFromEvent, + fireCustomEvent, + fireEvent, } from "../../dom/events.mjs"; -import {getDocument} from "../../dom/util.mjs"; -import {Formatter} from "../../text/formatter.mjs"; -import {getGlobal} from "../../types/global.mjs"; -import {ID} from "../../types/id.mjs"; +import { getDocument } from "../../dom/util.mjs"; +import { Formatter } from "../../text/formatter.mjs"; +import { getGlobal } from "../../types/global.mjs"; +import { ID } from "../../types/id.mjs"; import { - isArray, - isFunction, - isInteger, - isIterable, - isObject, - isPrimitive, - isString, + isArray, + isFunction, + isInteger, + isIterable, + isObject, + isPrimitive, + isString, } from "../../types/is.mjs"; -import {Observer} from "../../types/observer.mjs"; -import {ProxyObserver} from "../../types/proxyobserver.mjs"; -import {validateArray, validateString} from "../../types/validate.mjs"; -import {Processing} from "../../util/processing.mjs"; -import {STYLE_DISPLAY_MODE_BLOCK} from "./constants.mjs"; -import {SelectStyleSheet} from "./stylesheet/select.mjs"; +import { Observer } from "../../types/observer.mjs"; +import { ProxyObserver } from "../../types/proxyobserver.mjs"; +import { validateArray, validateString } from "../../types/validate.mjs"; +import { Processing } from "../../util/processing.mjs"; +import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs"; +import { SelectStyleSheet } from "./stylesheet/select.mjs"; import { - getDocumentTranslations, - Translations, + getDocumentTranslations, + Translations, } from "../../i18n/translations.mjs"; export { - Select, - popperElementSymbol, - getSummaryTemplate, - getSelectionTemplate, + Select, + popperElementSymbol, + getSummaryTemplate, + getSelectionTemplate, }; /** @@ -178,7 +179,7 @@ const popperFilterElementSymbol = Symbol("popperFilterElement"); * @type {symbol} */ const popperFilterContainerElementSymbol = Symbol( - "popperFilterContainerElement", + "popperFilterContainerElement", ); /** @@ -314,492 +315,488 @@ const FILTER_POSITION_INLINE = "inline"; * @fires Monster.Components.Form.event:monster-changed */ class Select extends CustomControl { - /** - * @extends CustomControl - */ - constructor() { - super(); - initOptionObserver.call(this); - } - - /** - * This method is called by the `instanceof` operator. - * @returns {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/select@@instance"); - } - - /** - * The current selection of the Select - * - * ``` - * e = document.querySelector('monster-select'); - * console.log(e.value) - * // ↦ 1 - * // ↦ ['1','2'] - * ``` - * - * @property {string|array} - */ - get value() { - return convertSelectionToValue.call(this, this.getOption("selection")); - } - - /** - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} - * @return {boolean} - */ - static get formAssociated() { - return true; - } - - /** - * Set selection - * - * ``` - * e = document.querySelector('monster-select'); - * e.value=1 - * ``` - * - * @property {string|array} value - * @since 1.2.0 - * @throws {Error} unsupported type - */ - set value(value) { - - const result = convertValueToSelection.call(this, value); - setSelection - .call(this, result.selection) - .then(() => { - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - - } - - /** - * To set the options via the html tag the attribute `data-monster-options` must be used. - * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} - * - * The individual configuration values can be found in the table. - * - * @property {Object} toggleEventType=click,touch List of event types to be observed for opening the dropdown - * @property {boolean} delegatesFocus=false lorem [see mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus) - * @property {Object[]} options Selection of key identifier pairs available for selection and displayed in the dropdown. - * @property {string} options[].label - * @property {string} options[].value - * @property {string} options[].visibility hidden or visible - * @property {Array} selection Selected options - * @property {Integer} showMaxOptions=10 Maximum number of visible options before a scroll bar should be displayed. - * @property {string} type=radio Multiple (checkbox) or single selection (radio) - * @property {string} name=(random id) Name of the form field - * @property {string} url Load options from server per url - * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) - * @property {String} fetch.redirect=error - * @property {String} fetch.method=GET - * @property {String} fetch.mode=same-origin - * @property {String} fetch.credentials=same-origin - * @property {Object} fetch.headers={"accept":"application/json"}} - * @property {Object} labels - * @property {string} labels.cannot-be-loaded cannot be loaded - * @property {string} labels.no-options-available no options available - * @property {string} labels.select-an-option select an option - * @property {string} labels.no-option no option in the list, maybe you have to change the filter - * @property {Object} features List with features - * @property {Boolean} features.clearAll=true Display of a delete button to delete the entire selection - * @property {Boolean} features.clear=true Display of a delete key for deleting the specific selection - * @property {Boolean} features.lazyLoad=false Load options when first opening the dropdown - * @property {Boolean} features.closeOnSelect=false Close the dropdown when an option is selected (since 3.54.0) - * @property {Boolean} features.emptyValueIfNoOptions=false If no options are available, the selection is set to an empty array - * @property {Boolean} filter.defaultValue=* Default filter value, if the filter is empty - * @property {Boolean} filter.mode=options Filter mode, values: options, remote, disabled - * @property {Object} templates Template definitions - * @property {string} templates.main Main template - * @property {string} templateMapping Mapping of the template placeholders - * @property {string} templateMapping.selected Selected Template - * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) - * @property {string} popper.placement=bottom PopperJS placement - * @property {Object[]} modifiers={name:offset} PopperJS placement - * @property {Object} mapping - * @property {String} mapping.selector=* Path to select the appropriate entries - * @property {String} mapping.labelTemplate="" template with the label placeholders in the form ${name}, where name is the key (**) - * @property {String} mapping.valueTemplate="" template with the value placeholders in the form ${name}, where name is the key - * @property {Monster.Components.Form~exampleFilterCallback|undefined} mapping.filter Filtering of values via a function - * @property {Object} formatter - * @property {Monster.Components.Form~formatterSelectionCallback|undefined} formatter.selection format selection label - */ - get defaults() { - return Object.assign( - {}, - super.defaults, - { - toggleEventType: ["click", "touch"], - delegatesFocus: false, - options: [], - selection: [], - showMaxOptions: 10, - type: "radio", - name: new ID("s").toString(), - features: { - clearAll: true, - clear: true, - lazyLoad: false, - closeOnSelect: false, - emptyValueIfNoOptions: false, - }, - url: null, - labels: { - "cannot-be-loaded": "Cannot be loaded", - "no-options-available": noOptionsAvailableMessage, - "click-to-load-options": clickToLoadOptionsMessage, - "select-an-option": "Select an option", - "summary-text": { - zero: "No entries were selected", - one: '<span class="monster-badge-primary-pill">1</span> entry was selected', - other: - '<span class="monster-badge-primary-pill">${count}</span> entries were selected', - }, - "no-options": - "Unfortunately, there are no options available in the list.", - "no-options-found": - "No options are available in the list. Please consider modifying the filter.", - }, - messages: { - control: null, - selected: null, - emptyOptions: null, - }, - fetch: { - redirect: "error", - method: "GET", - mode: "same-origin", - credentials: "same-origin", - headers: { - accept: "application/json", - }, - }, - filter: { - defaultValue: "*", - mode: FILTER_MODE_DISABLED, - position: FILTER_POSITION_INLINE, - }, - classes: { - badge: "monster-badge-primary", - statusOrRemoveBadge: "empty", - }, - mapping: { - selector: "*", - labelTemplate: "", - valueTemplate: "", - filter: null, - }, - formatter: { - selection: buildSelectionLabel, - }, - templates: { - main: getTemplate(), - }, - templateMapping: { - /** with the attribute `data-monster-selected-template` the template for the selected options can be defined. */ - selected: getSelectionTemplate(), - }, - - popper: { - placement: "bottom", - middleware: ["flip", "offset:1"], - }, - }, - initOptionsFromArguments.call(this), - ); - } - - /** - * @return {Monster.Components.Form.Select} - */ - [assembleMethodSymbol]() { - const self = this; - super[assembleMethodSymbol](); - - initControlReferences.call(self); - initEventHandler.call(self); - - const lazyLoadFlag = self.getOption("features.lazyLoad"); - - if (self.hasAttribute("value")) { - new Processing(10, () => { - this.value = this.getAttribute("value"); - }).run().catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - - if (self.getOption("url") !== null && !lazyLoadFlag) { - setStatusOrRemoveBadges.call(this, "loading"); - - new Processing(200, () => { - this.fetch() - .then(() => { - setTimeout(() => { - let result; - if (self.hasAttribute("value")) { - result = setSelection.call(self, self.getAttribute("value")); - } else { - result = setSelection.call(self, []); - } - - result - .then(() => { - }) - .catch((e) => { - addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - }, 100); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - setStatusOrRemoveBadges.call(this, "error"); - }); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - - let lastValue = self.value; - self[internalSymbol].attachObserver( - new Observer(function () { - - if (isObject(this) && this instanceof ProxyObserver) { - const n = this.getSubject()?.options?.value; - - if (lastValue !== n) { - lastValue = n; - setSelection - .call(self, n) - .then(() => { - }) - .catch((e) => { - addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - } - } - }), - ); - - areOptionsAvailableAndInit.call(self); - - return this; - } - - /** - * The Button.click() method simulates a click on the internal button element. - * - * @since 3.27.0 - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click} - */ - click() { - if (this.getOption("disabled") === true) { - return; - } - - toggle.call(this); - } - - /** - * The Button.focus() method sets focus on the internal button element. - * - * @since 3.27.0 - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} - */ - focus(options) { - if (this.getOption("disabled") === true) { - return; - } - - new Processing(() => { - gatherState.call(this); - focusFilter.call(this, options); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - } - - /** - * The Button.blur() method removes focus from the internal button element. - * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur - */ - blur() { - new Processing(() => { - gatherState.call(this); - blurFilter.call(this); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - } - - /** - * If no url is specified, the options are taken from the Component itself. - * - * @param {string|URL} url URL to fetch the options - * @return {Promise} - */ - fetch(url) { - if (url instanceof URL) { - url = url.toString(); - } - - if (url !== undefined && url !== null) { - url = validateString(url); - } - - return new Promise((resolve, reject) => { - const response = fetchData.call(this, url).then((map) => { - if ( - isObject(map) || - isArray(map) || - map instanceof Set || - map instanceof Map - ) { - this.importOptions(map); - setTimeout(() => { - resolve(); - }, 10); - return; - } - - reject(new Error("invalid response")); - }); - }); - } - - /** - * @return {void} - */ - connectedCallback() { - super.connectedCallback(); - const document = getDocument(); - - for (const [, type] of Object.entries(["click", "touch"])) { - // close on outside ui-events - document.addEventListener(type, this[closeEventHandler]); - } - - parseSlotsToOptions.call(this); - attachResizeObserver.call(this); - updatePopper.call(this); - - new Processing(() => { - gatherState.call(this); - focusFilter.call(this); - }).run(); - } - - /** - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - const document = getDocument(); - - // close on outside ui-events - for (const [, type] of Object.entries(["click", "touch"])) { - document.removeEventListener(type, this[closeEventHandler]); - } - - disconnectResizeObserver.call(this); - } - - /** - * Import Select Options from dataset - * Not to be confused with the control defaults/options - * - * @since 0.16.0 - * @param {array|object|Map|Set} data - * @return {Select} - * @throws {Error} map is not iterable - * @throws {Error} missing label configuration - */ - importOptions(data) { - const mappingOptions = this.getOption("mapping", {}); - const selector = mappingOptions?.["selector"]; - const labelTemplate = mappingOptions?.["labelTemplate"]; - const valueTemplate = mappingOptions?.["valueTemplate"]; - const filter = mappingOptions?.["filter"]; - - let flag = false; - if (labelTemplate === "") { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template"); - flag = true; - } - - if (valueTemplate === "") { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty value template"); - flag = true; - } - - if (flag === true) { - throw new Error("missing label configuration"); - } - - const map = buildMap(data, selector, labelTemplate, valueTemplate, filter); - - const options = []; - if (!isIterable(map)) { - throw new Error("map is not iterable"); - } - - const visibility = "visible"; - - map.forEach((label, value) => { - options.push({ - value, - label, - visibility, - }); - }); - - runAsOptionLengthChanged.call(this, map.size); - this.setOption("options", options); - - fireCustomEvent(this, "monster-options-set", { - options, - }); - - return this; - } - - /** - * @private - * @return {Monster.Components.Form.Select} - */ - calcAndSetOptionsDimension() { - calcAndSetOptionsDimension.call(this); - return this; - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-select"; - } - - /** - * - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [SelectStyleSheet]; - } + /** + * @extends CustomControl + */ + constructor() { + super(); + initOptionObserver.call(this); + } + + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/form/select@@instance"); + } + + /** + * The current selection of the Select + * + * ``` + * e = document.querySelector('monster-select'); + * console.log(e.value) + * // ↦ 1 + * // ↦ ['1','2'] + * ``` + * + * @property {string|array} + */ + get value() { + return convertSelectionToValue.call(this, this.getOption("selection")); + } + + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @return {boolean} + */ + static get formAssociated() { + return true; + } + + /** + * Set selection + * + * ``` + * e = document.querySelector('monster-select'); + * e.value=1 + * ``` + * + * @property {string|array} value + * @since 1.2.0 + * @throws {Error} unsupported type + */ + set value(value) { + const result = convertValueToSelection.call(this, value); + setSelection + .call(this, result.selection) + .then(() => {}) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + /** + * To set the options via the html tag the attribute `data-monster-options` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {Object} toggleEventType=click,touch List of event types to be observed for opening the dropdown + * @property {boolean} delegatesFocus=false lorem [see mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus) + * @property {Object[]} options Selection of key identifier pairs available for selection and displayed in the dropdown. + * @property {string} options[].label + * @property {string} options[].value + * @property {string} options[].visibility hidden or visible + * @property {Array} selection Selected options + * @property {Integer} showMaxOptions=10 Maximum number of visible options before a scroll bar should be displayed. + * @property {string} type=radio Multiple (checkbox) or single selection (radio) + * @property {string} name=(random id) Name of the form field + * @property {string} url Load options from server per url + * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) + * @property {String} fetch.redirect=error + * @property {String} fetch.method=GET + * @property {String} fetch.mode=same-origin + * @property {String} fetch.credentials=same-origin + * @property {Object} fetch.headers={"accept":"application/json"}} + * @property {Object} labels + * @property {string} labels.cannot-be-loaded cannot be loaded + * @property {string} labels.no-options-available no options available + * @property {string} labels.select-an-option select an option + * @property {string} labels.no-option no option in the list, maybe you have to change the filter + * @property {Object} features List with features + * @property {Boolean} features.clearAll=true Display of a delete button to delete the entire selection + * @property {Boolean} features.clear=true Display of a delete key for deleting the specific selection + * @property {Boolean} features.lazyLoad=false Load options when first opening the dropdown + * @property {Boolean} features.closeOnSelect=false Close the dropdown when an option is selected (since 3.54.0) + * @property {Boolean} features.emptyValueIfNoOptions=false If no options are available, the selection is set to an empty array + * @property {Boolean} filter.defaultValue=* Default filter value, if the filter is empty + * @property {Boolean} filter.mode=options Filter mode, values: options, remote, disabled + * @property {Object} templates Template definitions + * @property {string} templates.main Main template + * @property {string} templateMapping Mapping of the template placeholders + * @property {string} templateMapping.selected Selected Template + * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/) + * @property {string} popper.placement=bottom PopperJS placement + * @property {Object[]} modifiers={name:offset} PopperJS placement + * @property {Object} mapping + * @property {String} mapping.selector=* Path to select the appropriate entries + * @property {String} mapping.labelTemplate="" template with the label placeholders in the form ${name}, where name is the key (**) + * @property {String} mapping.valueTemplate="" template with the value placeholders in the form ${name}, where name is the key + * @property {Monster.Components.Form~exampleFilterCallback|undefined} mapping.filter Filtering of values via a function + * @property {Object} formatter + * @property {Monster.Components.Form~formatterSelectionCallback|undefined} formatter.selection format selection label + */ + get defaults() { + return Object.assign( + {}, + super.defaults, + { + toggleEventType: ["click", "touch"], + delegatesFocus: false, + options: [], + selection: [], + showMaxOptions: 10, + type: "radio", + name: new ID("s").toString(), + features: { + clearAll: true, + clear: true, + lazyLoad: false, + closeOnSelect: false, + emptyValueIfNoOptions: false, + }, + url: null, + labels: { + "cannot-be-loaded": "Cannot be loaded", + "no-options-available": noOptionsAvailableMessage, + "click-to-load-options": clickToLoadOptionsMessage, + "select-an-option": "Select an option", + "summary-text": { + zero: "No entries were selected", + one: '<span class="monster-badge-primary-pill">1</span> entry was selected', + other: + '<span class="monster-badge-primary-pill">${count}</span> entries were selected', + }, + "no-options": + "Unfortunately, there are no options available in the list.", + "no-options-found": + "No options are available in the list. Please consider modifying the filter.", + }, + messages: { + control: null, + selected: null, + emptyOptions: null, + }, + fetch: { + redirect: "error", + method: "GET", + mode: "same-origin", + credentials: "same-origin", + headers: { + accept: "application/json", + }, + }, + filter: { + defaultValue: "*", + mode: FILTER_MODE_DISABLED, + position: FILTER_POSITION_INLINE, + }, + classes: { + badge: "monster-badge-primary", + statusOrRemoveBadge: "empty", + }, + mapping: { + selector: "*", + labelTemplate: "", + valueTemplate: "", + filter: null, + }, + formatter: { + selection: buildSelectionLabel, + }, + templates: { + main: getTemplate(), + }, + templateMapping: { + /** with the attribute `data-monster-selected-template` the template for the selected options can be defined. */ + selected: getSelectionTemplate(), + }, + + popper: { + placement: "bottom", + middleware: ["flip", "offset:1"], + }, + }, + initOptionsFromArguments.call(this), + ); + } + + /** + * @return {Monster.Components.Form.Select} + */ + [assembleMethodSymbol]() { + const self = this; + super[assembleMethodSymbol](); + + initControlReferences.call(self); + initEventHandler.call(self); + + const lazyLoadFlag = self.getOption("features.lazyLoad"); + + if (self.hasAttribute("value")) { + new Processing(10, () => { + this.value = this.getAttribute("value"); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + if (self.getOption("url") !== null && !lazyLoadFlag) { + setStatusOrRemoveBadges.call(this, "loading"); + + new Processing(200, () => { + this.fetch() + .then(() => { + setTimeout(() => { + let result; + if (self.hasAttribute("value")) { + result = setSelection.call(self, self.getAttribute("value")); + } else { + result = setSelection.call(self, []); + } + + result + .then(() => {}) + .catch((e) => { + addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + }, 100); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + setStatusOrRemoveBadges.call(this, "error"); + }); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + let lastValue = self.value; + self[internalSymbol].attachObserver( + new Observer(function () { + if (isObject(this) && this instanceof ProxyObserver) { + const n = this.getSubject()?.options?.value; + + if (lastValue !== n) { + lastValue = n; + setSelection + .call(self, n) + .then(() => {}) + .catch((e) => { + addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + } + } + }), + ); + + areOptionsAvailableAndInit.call(self); + + return this; + } + + /** + * The Button.click() method simulates a click on the internal button element. + * + * @since 3.27.0 + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click} + */ + click() { + if (this.getOption("disabled") === true) { + return; + } + + toggle.call(this); + } + + /** + * The Button.focus() method sets focus on the internal button element. + * + * @since 3.27.0 + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} + */ + focus(options) { + if (this.getOption("disabled") === true) { + return; + } + + new Processing(() => { + gatherState.call(this); + focusFilter.call(this, options); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + } + + /** + * The Button.blur() method removes focus from the internal button element. + * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur + */ + blur() { + new Processing(() => { + gatherState.call(this); + blurFilter.call(this); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + } + + /** + * If no url is specified, the options are taken from the Component itself. + * + * @param {string|URL} url URL to fetch the options + * @return {Promise} + */ + fetch(url) { + if (url instanceof URL) { + url = url.toString(); + } + + if (url !== undefined && url !== null) { + url = validateString(url); + } + + return new Promise((resolve, reject) => { + const response = fetchData.call(this, url).then((map) => { + if ( + isObject(map) || + isArray(map) || + map instanceof Set || + map instanceof Map + ) { + this.importOptions(map); + setTimeout(() => { + resolve(); + }, 10); + return; + } + + reject(new Error("invalid response")); + }); + }); + } + + /** + * @return {void} + */ + connectedCallback() { + super.connectedCallback(); + const document = getDocument(); + + for (const [, type] of Object.entries(["click", "touch"])) { + // close on outside ui-events + document.addEventListener(type, this[closeEventHandler]); + } + + parseSlotsToOptions.call(this); + attachResizeObserver.call(this); + updatePopper.call(this); + + new Processing(() => { + gatherState.call(this); + focusFilter.call(this); + }).run(); + } + + /** + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + const document = getDocument(); + + // close on outside ui-events + for (const [, type] of Object.entries(["click", "touch"])) { + document.removeEventListener(type, this[closeEventHandler]); + } + + disconnectResizeObserver.call(this); + } + + /** + * Import Select Options from dataset + * Not to be confused with the control defaults/options + * + * @since 0.16.0 + * @param {array|object|Map|Set} data + * @return {Select} + * @throws {Error} map is not iterable + * @throws {Error} missing label configuration + */ + importOptions(data) { + const mappingOptions = this.getOption("mapping", {}); + const selector = mappingOptions?.["selector"]; + const labelTemplate = mappingOptions?.["labelTemplate"]; + const valueTemplate = mappingOptions?.["valueTemplate"]; + const filter = mappingOptions?.["filter"]; + + let flag = false; + if (labelTemplate === "") { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template"); + flag = true; + } + + if (valueTemplate === "") { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty value template"); + flag = true; + } + + if (flag === true) { + throw new Error("missing label configuration"); + } + + const map = buildMap(data, selector, labelTemplate, valueTemplate, filter); + + const options = []; + if (!isIterable(map)) { + throw new Error("map is not iterable"); + } + + const visibility = "visible"; + + map.forEach((label, value) => { + options.push({ + value, + label, + visibility, + }); + }); + + runAsOptionLengthChanged.call(this, map.size); + this.setOption("options", options); + + fireCustomEvent(this, "monster-options-set", { + options, + }); + + return this; + } + + /** + * @private + * @return {Monster.Components.Form.Select} + */ + calcAndSetOptionsDimension() { + calcAndSetOptionsDimension.call(this); + return this; + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-select"; + } + + /** + * + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [SelectStyleSheet]; + } } /** @@ -814,64 +811,64 @@ class Select extends CustomControl { * @return {object} */ function initOptionsFromArguments() { - const options = {}; - - const template = this.getAttribute("data-monster-selected-template"); - if (isString(template)) { - if (!options["templateMapping"]) options["templateMapping"] = {}; - - switch (template) { - case "summary": - case "default": - options["templateMapping"]["selected"] = getSummaryTemplate(); - break; - case "selected": - options["templateMapping"]["selected"] = getSelectionTemplate(); - break; - default: - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - "invalid template, use summary or selected", - ); - } - } - - return options; + const options = {}; + + const template = this.getAttribute("data-monster-selected-template"); + if (isString(template)) { + if (!options["templateMapping"]) options["templateMapping"] = {}; + + switch (template) { + case "summary": + case "default": + options["templateMapping"]["selected"] = getSummaryTemplate(); + break; + case "selected": + options["templateMapping"]["selected"] = getSelectionTemplate(); + break; + default: + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + "invalid template, use summary or selected", + ); + } + } + + return options; } /** * @private */ function attachResizeObserver() { - // against flickering - this[resizeObserverSymbol] = new ResizeObserver((entries) => { - if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { - try { - this[timerCallbackSymbol].touch(); - return; - } catch (e) { - delete this[timerCallbackSymbol]; - } - } - - this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { - updatePopper.call(this); - delete this[timerCallbackSymbol]; - }); - }); - - this[resizeObserverSymbol].observe(this.parentElement); + // against flickering + this[resizeObserverSymbol] = new ResizeObserver((entries) => { + if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[timerCallbackSymbol].touch(); + return; + } catch (e) { + delete this[timerCallbackSymbol]; + } + } + + this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { + updatePopper.call(this); + delete this[timerCallbackSymbol]; + }); + }); + + this[resizeObserverSymbol].observe(this.parentElement); } function disconnectResizeObserver() { - if (this[resizeObserverSymbol] instanceof ResizeObserver) { - this[resizeObserverSymbol].disconnect(); - } + if (this[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } } function getSelectionTemplate() { - return `<div data-monster-role="selection" + return `<div data-monster-role="selection" data-monster-insert="selection path:selection" role="search" ><input type="text" role="searchbox" part="inline-filter" name="inline-filter" @@ -883,7 +880,7 @@ function getSelectionTemplate() { } function getSummaryTemplate() { - return `<div data-monster-role="selection" role="search"> + return `<div data-monster-role="selection" role="search"> <input type="text" role="searchbox" part="inline-filter" name="inline-filter" data-monster-role="filter" @@ -899,35 +896,35 @@ function getSummaryTemplate() { * @private */ function parseSlotsToOptions() { - let options = this.getOption("options"); - if (!isIterable(options)) { - options = []; - } - - let counter = 1; - getSlottedElements.call(this, "div").forEach((node) => { - let value = (counter++).toString(); - let visibility = "visible"; - - if (node.hasAttribute("data-monster-value")) { - value = node.getAttribute("data-monster-value"); - } - - if (node.style.display === "none") { - visibility = "hidden"; - } - - const label = node.outerHTML; - - options.push({ - value, - label, - visibility, - }); - }); - - runAsOptionLengthChanged.call(this, options.length); - this.setOption("options", options); + let options = this.getOption("options"); + if (!isIterable(options)) { + options = []; + } + + let counter = 1; + getSlottedElements.call(this, "div").forEach((node) => { + let value = (counter++).toString(); + let visibility = "visible"; + + if (node.hasAttribute("data-monster-value")) { + value = node.getAttribute("data-monster-value"); + } + + if (node.style.display === "none") { + visibility = "hidden"; + } + + const label = node.outerHTML; + + options.push({ + value, + label, + visibility, + }); + }); + + runAsOptionLengthChanged.call(this, options.length); + this.setOption("options", options); } /** @@ -937,39 +934,39 @@ function parseSlotsToOptions() { * @param {int} targetLength */ function runAsOptionLengthChanged(targetLength) { - const self = this; - - if (!self[optionsElementSymbol]) { - return; - } - - const callback = function (mutationsList, observer) { - const run = false; - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - const run = true; - break; - } - } - - if (run === true) { - const nodes = self[optionsElementSymbol].querySelectorAll( - `div[${ATTRIBUTE_ROLE}=option]`, - ); - - if (nodes.length === targetLength) { - checkOptionState.call(self); - observer.disconnect(); - } - } - }; - - const observer = new MutationObserver(callback); - observer.observe(self[optionsElementSymbol], { - attributes: false, - childList: true, - subtree: true, - }); + const self = this; + + if (!self[optionsElementSymbol]) { + return; + } + + const callback = function (mutationsList, observer) { + const run = false; + for (const mutation of mutationsList) { + if (mutation.type === "childList") { + const run = true; + break; + } + } + + if (run === true) { + const nodes = self[optionsElementSymbol].querySelectorAll( + `div[${ATTRIBUTE_ROLE}=option]`, + ); + + if (nodes.length === targetLength) { + checkOptionState.call(self); + observer.disconnect(); + } + } + }; + + const observer = new MutationObserver(callback); + observer.observe(self[optionsElementSymbol], { + attributes: false, + childList: true, + subtree: true, + }); } /** @@ -978,18 +975,18 @@ function runAsOptionLengthChanged(targetLength) { * @return {*} */ function buildSelectionLabel(value) { - const options = this.getOption("options"); - - for (let i = 0; i < options.length; i++) { - const o = options?.[i]; - if (isObject(o) && o?.["value"] === value) { - return o?.["label"]; - } else if (isPrimitive(o) && o === value) { - return o; - } - } - - return undefined; + const options = this.getOption("options"); + + for (let i = 0; i < options.length; i++) { + const o = options?.[i]; + if (isObject(o) && o?.["value"] === value) { + return o?.["label"]; + } else if (isPrimitive(o) && o === value) { + return o; + } + } + + return undefined; } /** @@ -999,17 +996,17 @@ function buildSelectionLabel(value) { * @throws {Error} no value found */ function getSelectionLabel(value) { - const callback = this.getOption("formatter.selection"); - if (isFunction(callback)) { - const label = callback.call(this, value); - if (isString(label)) return label; - } + const callback = this.getOption("formatter.selection"); + if (isFunction(callback)) { + const label = callback.call(this, value); + if (isString(label)) return label; + } - if (isString(value) || isInteger(value)) { - return `${value}`; - } + if (isString(value) || isInteger(value)) { + return `${value}`; + } - return this.getOption("labels.cannot-be-loaded", value); + return this.getOption("labels.cannot-be-loaded", value); } /** @@ -1017,25 +1014,25 @@ function getSelectionLabel(value) { * @param {Event} event */ function handleToggleKeyboardEvents(event) { - switch (event?.["code"]) { - case "Escape": - toggle.call(this); - event.preventDefault(); - break; - case "Space": - toggle.call(this); - event.preventDefault(); - break; - case "ArrowDown": - show.call(this); - activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); - event.preventDefault(); - break; - case "ArrowUp": - hide.call(this); - event.preventDefault(); - break; - } + switch (event?.["code"]) { + case "Escape": + toggle.call(this); + event.preventDefault(); + break; + case "Space": + toggle.call(this); + event.preventDefault(); + break; + case "ArrowDown": + show.call(this); + activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); + event.preventDefault(); + break; + case "ArrowUp": + hide.call(this); + event.preventDefault(); + break; + } } /** @@ -1045,38 +1042,35 @@ function handleToggleKeyboardEvents(event) { * @this CustomElement */ function initOptionObserver() { - const self = this; - - self.attachObserver( - new Observer(function () { - new Processing(() => { - try { - self.updateI18n(); - } catch (e) { - } - try { - areOptionsAvailableAndInit.call(self); - } catch (e) { - } - - setSummaryAndControlText.call(self); - }).run(); - }), - ); + const self = this; + + self.attachObserver( + new Observer(function () { + new Processing(() => { + try { + self.updateI18n(); + } catch (e) {} + try { + areOptionsAvailableAndInit.call(self); + } catch (e) {} + + setSummaryAndControlText.call(self); + }).run(); + }), + ); } function getDefaultTranslation() { - const translation = new Translations("en").assignTranslations( - this.getOption("labels", {}), - ); + const translation = new Translations("en").assignTranslations( + this.getOption("labels", {}), + ); - try { - const doc = getDocumentTranslations(); - translation.locale = doc.locale; - } catch (e) { - } + try { + const doc = getDocumentTranslations(); + translation.locale = doc.locale; + } catch (e) {} - return translation; + return translation; } /** @@ -1084,36 +1078,36 @@ function getDefaultTranslation() { * @returns {string|*} */ function setSummaryAndControlText() { - const translations = getDefaultTranslation.call(this); - const selections = this.getOption("selection"); - - const text = translations.getPluralRuleText( - "summary-text", - selections.length, - "", - ); - - const selectedText = new Formatter({ - count: String(selections.length), - }).format(text); - - this.setOption("messages.selected", selectedText); - - const current = this.getOption("messages.control"); - const msg = this.getOption("labels.select-an-option"); - - if ( - current === "" || - current === undefined || - current === msg || - current === null - ) { - if (selections === undefined || selections.length === 0) { - this.setOption("messages.control", msg); - } else { - this.setOption("messages.control", ""); - } - } + const translations = getDefaultTranslation.call(this); + const selections = this.getOption("selection"); + + const text = translations.getPluralRuleText( + "summary-text", + selections.length, + "", + ); + + const selectedText = new Formatter({ + count: String(selections.length), + }).format(text); + + this.setOption("messages.selected", selectedText); + + const current = this.getOption("messages.control"); + const msg = this.getOption("labels.select-an-option"); + + if ( + current === "" || + current === undefined || + current === msg || + current === null + ) { + if (selections === undefined || selections.length === 0) { + this.setOption("messages.control", msg); + } else { + this.setOption("messages.control", ""); + } + } } /** @@ -1121,9 +1115,9 @@ function setSummaryAndControlText() { * @return {NodeList} */ function getOptionElements() { - return this[optionsElementSymbol].querySelectorAll( - `[${ATTRIBUTE_ROLE}=option]`, - ); + return this[optionsElementSymbol].querySelectorAll( + `[${ATTRIBUTE_ROLE}=option]`, + ); } /** @@ -1149,82 +1143,82 @@ function getOptionElements() { * @private */ function calcAndSetOptionsDimension() { - const options = getOptionElements.call(this); - const container = this[optionsElementSymbol]; - if (!(container instanceof HTMLElement && options instanceof NodeList)) { - return; - } - - let visible = 0; - let optionHeight = 0; - const max = this.getOption("showMaxOptions", 10); - - let scrollFlag = false; - for (const [, option] of Object.entries(options)) { - const computedStyle = getGlobal().getComputedStyle(option); - if (computedStyle.display === "none") continue; - - let h = option.getBoundingClientRect().height; - h += parseInt(computedStyle.getPropertyValue("margin-top"), 10); - h += parseInt(computedStyle.getPropertyValue("margin-bottom"), 10); - optionHeight += h; - - visible++; - - if (visible > max) { - break; - } - } - - if (visible > max) { - visible = max; - scrollFlag = true; - } - - if (visible === 0) { - if (this.getOption("options").length === 0) { - this.setOption( - "messages.emptyOptions", - this.getOption("labels.no-options-available"), - ); - } else { - if (this.getOption("filter.mode") === FILTER_MODE_DISABLED) { - this.setOption( - "messages.emptyOptions", - this.getOption("labels.no-options-available"), - ); - } else { - this.setOption( - "messages.emptyOptions", - this.getOption("labels.no-options-found"), - ); - } - } - this[noOptionsAvailableElementSymbol].classList.remove("d-none"); - } else { - this[noOptionsAvailableElementSymbol].classList.add("d-none"); - } - - const styles = getGlobal().getComputedStyle(this[optionsElementSymbol]); - let padding = parseInt(styles.getPropertyValue("padding-top"), 10); - padding += parseInt(styles.getPropertyValue("padding-bottom"), 10); - - let margin = parseInt(styles.getPropertyValue("margin-top"), 10); - margin += parseInt(styles.getPropertyValue("margin-bottom"), 10); - - const containerHeight = optionHeight + padding + margin; - container.style.height = `${containerHeight}px`; - - if (scrollFlag === true) { - container.style.overflowY = "scroll"; - } else { - container.style.overflowY = "auto"; - } - - const domRect = this[controlElementSymbol].getBoundingClientRect(); - - this[popperElementSymbol].style.width = `${domRect.width}px`; - container.style.overflowX = "auto"; + const options = getOptionElements.call(this); + const container = this[optionsElementSymbol]; + if (!(container instanceof HTMLElement && options instanceof NodeList)) { + return; + } + + let visible = 0; + let optionHeight = 0; + const max = this.getOption("showMaxOptions", 10); + + let scrollFlag = false; + for (const [, option] of Object.entries(options)) { + const computedStyle = getGlobal().getComputedStyle(option); + if (computedStyle.display === "none") continue; + + let h = option.getBoundingClientRect().height; + h += parseInt(computedStyle.getPropertyValue("margin-top"), 10); + h += parseInt(computedStyle.getPropertyValue("margin-bottom"), 10); + optionHeight += h; + + visible++; + + if (visible > max) { + break; + } + } + + if (visible > max) { + visible = max; + scrollFlag = true; + } + + if (visible === 0) { + if (this.getOption("options").length === 0) { + this.setOption( + "messages.emptyOptions", + this.getOption("labels.no-options-available"), + ); + } else { + if (this.getOption("filter.mode") === FILTER_MODE_DISABLED) { + this.setOption( + "messages.emptyOptions", + this.getOption("labels.no-options-available"), + ); + } else { + this.setOption( + "messages.emptyOptions", + this.getOption("labels.no-options-found"), + ); + } + } + this[noOptionsAvailableElementSymbol].classList.remove("d-none"); + } else { + this[noOptionsAvailableElementSymbol].classList.add("d-none"); + } + + const styles = getGlobal().getComputedStyle(this[optionsElementSymbol]); + let padding = parseInt(styles.getPropertyValue("padding-top"), 10); + padding += parseInt(styles.getPropertyValue("padding-bottom"), 10); + + let margin = parseInt(styles.getPropertyValue("margin-top"), 10); + margin += parseInt(styles.getPropertyValue("margin-bottom"), 10); + + const containerHeight = optionHeight + padding + margin; + container.style.height = `${containerHeight}px`; + + if (scrollFlag === true) { + container.style.overflowY = "scroll"; + } else { + container.style.overflowY = "auto"; + } + + const domRect = this[controlElementSymbol].getBoundingClientRect(); + + this[popperElementSymbol].style.width = `${domRect.width}px`; + container.style.overflowX = "auto"; } /** @@ -1233,124 +1227,126 @@ function calcAndSetOptionsDimension() { * @throws {Error} no shadow-root is defined */ function activateCurrentOption(direction) { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - let focused = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`); - - if ( - !(focused instanceof HTMLElement) || - focused.matches("[data-monster-visibility=hidden]") - ) { - for (const [, e] of Object.entries( - this.shadowRoot.querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`), - )) { - if (e.matches("[data-monster-visibility=visible]")) { - focused = e; - break; - } - } - } else { - if (direction === FOCUS_DIRECTION_DOWN) { - while (focused.nextSibling) { - focused = focused.nextSibling; - - if ( - focused instanceof HTMLElement && - focused.hasAttribute(ATTRIBUTE_ROLE) && - focused.getAttribute(ATTRIBUTE_ROLE) === "option" && - focused.matches("[data-monster-visibility=visible]") && - focused.matches(":not([data-monster-filtered=true])") - ) { - break; - } - } - } else { - let found = false; - while (focused.previousSibling) { - focused = focused.previousSibling; - if ( - focused instanceof HTMLElement && - focused.hasAttribute(ATTRIBUTE_ROLE) && - focused.getAttribute(ATTRIBUTE_ROLE) === "option" && - focused.matches("[data-monster-visibility=visible]") && - focused.matches(":not([data-monster-filtered=true])") - ) { - found = true; - break; - } - } - if (found === false) { - focusFilter.call(this); - } - } - } - - new Processing(() => { - if (focused instanceof HTMLElement) { - this.shadowRoot - .querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`) - .forEach((e) => { - e.removeAttribute(`${ATTRIBUTE_PREFIX}focused`); - }); - - focused.focus(); - focused.setAttribute(`${ATTRIBUTE_PREFIX}focused`, true); - } - }).run().catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + let focused = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`); + + if ( + !(focused instanceof HTMLElement) || + focused.matches("[data-monster-visibility=hidden]") + ) { + for (const [, e] of Object.entries( + this.shadowRoot.querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`), + )) { + if (e.matches("[data-monster-visibility=visible]")) { + focused = e; + break; + } + } + } else { + if (direction === FOCUS_DIRECTION_DOWN) { + while (focused.nextSibling) { + focused = focused.nextSibling; + + if ( + focused instanceof HTMLElement && + focused.hasAttribute(ATTRIBUTE_ROLE) && + focused.getAttribute(ATTRIBUTE_ROLE) === "option" && + focused.matches("[data-monster-visibility=visible]") && + focused.matches(":not([data-monster-filtered=true])") + ) { + break; + } + } + } else { + let found = false; + while (focused.previousSibling) { + focused = focused.previousSibling; + if ( + focused instanceof HTMLElement && + focused.hasAttribute(ATTRIBUTE_ROLE) && + focused.getAttribute(ATTRIBUTE_ROLE) === "option" && + focused.matches("[data-monster-visibility=visible]") && + focused.matches(":not([data-monster-filtered=true])") + ) { + found = true; + break; + } + } + if (found === false) { + focusFilter.call(this); + } + } + } + + new Processing(() => { + if (focused instanceof HTMLElement) { + this.shadowRoot + .querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`) + .forEach((e) => { + e.removeAttribute(`${ATTRIBUTE_PREFIX}focused`); + }); + + focused.focus(); + focused.setAttribute(`${ATTRIBUTE_PREFIX}focused`, true); + } + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); } /** * @private */ function filterOptions() { - new Processing(() => { - let filterValue; - - switch (this.getOption("filter.position")) { - case FILTER_POSITION_INLINE: - if (this[inlineFilterElementSymbol] instanceof HTMLElement) { - filterValue = this[inlineFilterElementSymbol].value.toLowerCase(); - } else { - return; - } - - break; - case FILTER_POSITION_POPPER: - default: - if (this[popperFilterElementSymbol] instanceof HTMLInputElement) { - filterValue = this[popperFilterElementSymbol].value.toLowerCase(); - } else { - return; - } - } - - const options = this.getOption("options"); - for (const [i, option] of Object.entries(options)) { - if (option.label.toLowerCase().indexOf(filterValue) === -1) { - this.setOption(`options.${i}.filtered`, "true"); - } else { - this.setOption(`options.${i}.filtered`, undefined); - } - } - }) - .run() - .then(() => { - new Processing(100, () => { - calcAndSetOptionsDimension.call(this); - focusFilter.call(this); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); + new Processing(() => { + let filterValue; + + switch (this.getOption("filter.position")) { + case FILTER_POSITION_INLINE: + if (this[inlineFilterElementSymbol] instanceof HTMLElement) { + filterValue = this[inlineFilterElementSymbol].value.toLowerCase(); + } else { + return; + } + + break; + case FILTER_POSITION_POPPER: + default: + if (this[popperFilterElementSymbol] instanceof HTMLInputElement) { + filterValue = this[popperFilterElementSymbol].value.toLowerCase(); + } else { + return; + } + } + + const options = this.getOption("options"); + for (const [i, option] of Object.entries(options)) { + if (option.label.toLowerCase().indexOf(filterValue) === -1) { + this.setOption(`options.${i}.filtered`, "true"); + } else { + this.setOption(`options.${i}.filtered`, undefined); + } + } + }) + .run() + .then(() => { + new Processing(100, () => { + calcAndSetOptionsDimension.call(this); + focusFilter.call(this); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); } /** @@ -1358,37 +1354,37 @@ function filterOptions() { * @param {Event} event */ function handleFilterKeyboardEvents(event) { - const shiftKey = event?.["shiftKey"]; - - switch (event?.["code"]) { - case "Tab": - activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); - event.preventDefault(); - break; - case "Escape": - toggle.call(this); - event.preventDefault(); - break; - case "Tab" && shiftKey === true: - case "ArrowUp": - activateCurrentOption.call(this, FOCUS_DIRECTION_UP); - event.preventDefault(); - break; - case "Tab" && !shiftKey: - case "ArrowDown": - activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); - event.preventDefault(); - break; - default: - if ( - this.getOption("features.lazyLoad") === true && - this[lazyLoadDoneSymbol] !== true - ) { - this.click(); - } - - handleFilterKeyEvents.call(this); - } + const shiftKey = event?.["shiftKey"]; + + switch (event?.["code"]) { + case "Tab": + activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); + event.preventDefault(); + break; + case "Escape": + toggle.call(this); + event.preventDefault(); + break; + case "Tab" && shiftKey === true: + case "ArrowUp": + activateCurrentOption.call(this, FOCUS_DIRECTION_UP); + event.preventDefault(); + break; + case "Tab" && !shiftKey: + case "ArrowDown": + activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); + event.preventDefault(); + break; + default: + if ( + this.getOption("features.lazyLoad") === true && + this[lazyLoadDoneSymbol] !== true + ) { + this.click(); + } + + handleFilterKeyEvents.call(this); + } } /** @@ -1407,63 +1403,63 @@ function handleFilterKeyboardEvents(event) { * @returns {void} This method does not return anything. */ function handleFilterKeyEvents() { - if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) { - try { - this[keyFilterEventSymbol].touch(); - return; - } catch (e) { - delete this[keyFilterEventSymbol]; - } - } - - this[keyFilterEventSymbol] = new DeadMansSwitch(200, () => { - if (this.getOption("filter.mode") !== FILTER_MODE_REMOTE) { - filterOptions.call(this); - } else { - filterFromRemote.call(this).catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - - delete this[keyFilterEventSymbol]; - }); + if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) { + try { + this[keyFilterEventSymbol].touch(); + return; + } catch (e) { + delete this[keyFilterEventSymbol]; + } + } + + this[keyFilterEventSymbol] = new DeadMansSwitch(200, () => { + if (this.getOption("filter.mode") !== FILTER_MODE_REMOTE) { + filterOptions.call(this); + } else { + filterFromRemote.call(this).catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + + delete this[keyFilterEventSymbol]; + }); } /** * @private */ function filterFromRemote() { - if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) { - return; - } - - const optionUrl = this.getOption("url"); - if (!optionUrl) { - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - "Missing URL for Remote Filter.", - ); - return; - } - - return new Processing(() => { - const filterValue = encodeURI( - this[inlineFilterElementSymbol].value.toLowerCase(), - ); - let url = optionUrl; - if (filterValue.length > 0) { - url = new Formatter({filter: filterValue}).format(optionUrl); - } - - this.fetch(url) - .then(() => { - checkOptionState.call(this); - }) - .catch((e) => { - throw e; - }); - }).run(); + if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) { + return; + } + + const optionUrl = this.getOption("url"); + if (!optionUrl) { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + "Missing URL for Remote Filter.", + ); + return; + } + + return new Processing(() => { + const filterValue = encodeURI( + this[inlineFilterElementSymbol].value.toLowerCase(), + ); + let url = optionUrl; + if (filterValue.length > 0) { + url = new Formatter({ filter: filterValue }).format(optionUrl); + } + + this.fetch(url) + .then(() => { + checkOptionState.call(this); + }) + .catch((e) => { + throw e; + }); + }).run(); } /** @@ -1472,45 +1468,45 @@ function filterFromRemote() { * @private */ function handleOptionKeyboardEvents(event) { - const shiftKey = event?.["shiftKey"]; - - switch (event?.["code"]) { - case "Escape": - toggle.call(this); - event.preventDefault(); - break; - case "Enter": - case "Space": - const path = event.composedPath(); - const element = path?.[0]; - - fireEvent(element.getElementsByTagName("input"), "click"); - event.preventDefault(); - break; - - case "Tab" && shiftKey === true: - case "ArrowUp": - activateCurrentOption.call(this, FOCUS_DIRECTION_UP); - event.preventDefault(); - break; - - case "Tab" && !shiftKey: - case "ArrowLeft": - case "ArrowRight": - // handled by tree select - break; - case "ArrowDown": - activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); - event.preventDefault(); - break; - default: - const p = event.composedPath(); - if (p?.[0] instanceof HTMLInputElement) { - return; - } - focusFilter.call(this); - break; - } + const shiftKey = event?.["shiftKey"]; + + switch (event?.["code"]) { + case "Escape": + toggle.call(this); + event.preventDefault(); + break; + case "Enter": + case "Space": + const path = event.composedPath(); + const element = path?.[0]; + + fireEvent(element.getElementsByTagName("input"), "click"); + event.preventDefault(); + break; + + case "Tab" && shiftKey === true: + case "ArrowUp": + activateCurrentOption.call(this, FOCUS_DIRECTION_UP); + event.preventDefault(); + break; + + case "Tab" && !shiftKey: + case "ArrowLeft": + case "ArrowRight": + // handled by tree select + break; + case "ArrowDown": + activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN); + event.preventDefault(); + break; + default: + const p = event.composedPath(); + if (p?.[0] instanceof HTMLInputElement) { + return; + } + focusFilter.call(this); + break; + } } /** @@ -1518,33 +1514,33 @@ function handleOptionKeyboardEvents(event) { * @returns {string} */ function getFilterMode() { - switch (this.getOption("filter.mode")) { - case FILTER_MODE_OPTIONS: - return FILTER_MODE_OPTIONS; - case FILTER_MODE_REMOTE: - return FILTER_MODE_REMOTE; - default: - return FILTER_MODE_DISABLED; - } + switch (this.getOption("filter.mode")) { + case FILTER_MODE_OPTIONS: + return FILTER_MODE_OPTIONS; + case FILTER_MODE_REMOTE: + return FILTER_MODE_REMOTE; + default: + return FILTER_MODE_DISABLED; + } } /** * @private */ function blurFilter() { - if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) { - return; - } + if (!(this[inlineFilterElementSymbol] instanceof HTMLElement)) { + return; + } - if (getFilterMode.call(this) === FILTER_MODE_DISABLED) { - return; - } + if (getFilterMode.call(this) === FILTER_MODE_DISABLED) { + return; + } - this[popperFilterContainerElementSymbol].classList.remove("active"); - this[popperFilterContainerElementSymbol].blur(); + this[popperFilterContainerElementSymbol].classList.remove("active"); + this[popperFilterContainerElementSymbol].blur(); - this[inlineFilterElementSymbol].classList.remove("active"); - this[inlineFilterElementSymbol].blur(); + this[inlineFilterElementSymbol].classList.remove("active"); + this[inlineFilterElementSymbol].blur(); } /** @@ -1552,29 +1548,29 @@ function blurFilter() { * @param focusOptions */ function focusPopperFilter(focusOptions) { - this[popperFilterContainerElementSymbol].classList.remove("d-none"); - this[popperFilterElementSymbol].classList.add("active"); - this[inlineFilterElementSymbol].classList.remove("active"); - this[inlineFilterElementSymbol].classList.add("d-none"); - - if (!(this[popperFilterElementSymbol] instanceof HTMLElement)) { - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - "Missing Popper Filter Element.", - ); - return; - } - - // visibility is set to visible, because focus() does not work on invisible elements - // and the class definition is assigned later in the processing - setTimeout(() => { - if (focusOptions === undefined || focusOptions === null) { - this[popperFilterElementSymbol].focus(); - } else { - this[popperFilterElementSymbol].focus(focusOptions); - } - }, 100); + this[popperFilterContainerElementSymbol].classList.remove("d-none"); + this[popperFilterElementSymbol].classList.add("active"); + this[inlineFilterElementSymbol].classList.remove("active"); + this[inlineFilterElementSymbol].classList.add("d-none"); + + if (!(this[popperFilterElementSymbol] instanceof HTMLElement)) { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + "Missing Popper Filter Element.", + ); + return; + } + + // visibility is set to visible, because focus() does not work on invisible elements + // and the class definition is assigned later in the processing + setTimeout(() => { + if (focusOptions === undefined || focusOptions === null) { + this[popperFilterElementSymbol].focus(); + } else { + this[popperFilterElementSymbol].focus(focusOptions); + } + }, 100); } /** @@ -1582,44 +1578,44 @@ function focusPopperFilter(focusOptions) { * @param focusOptions */ function focusInlineFilter(focusOptions) { - const options = this.getOption("options"); - if ( - (!isArray(options) || options.length === 0) && - this.getOption("filter.mode") !== FILTER_MODE_REMOTE - ) { - return; - } - - this[popperFilterContainerElementSymbol].classList.add("d-none"); - this[inlineFilterElementSymbol].classList.add("active"); - this[inlineFilterElementSymbol].classList.remove("d-none"); - - // visibility is set to visible, because focus() does not work on invisible elements - // and the class definition is assigned later in the processing - setTimeout(() => { - if (focusOptions === undefined || focusOptions === null) { - this[inlineFilterElementSymbol].focus(); - } else { - this[inlineFilterElementSymbol].focus(focusOptions); - } - }, 100); + const options = this.getOption("options"); + if ( + (!isArray(options) || options.length === 0) && + this.getOption("filter.mode") !== FILTER_MODE_REMOTE + ) { + return; + } + + this[popperFilterContainerElementSymbol].classList.add("d-none"); + this[inlineFilterElementSymbol].classList.add("active"); + this[inlineFilterElementSymbol].classList.remove("d-none"); + + // visibility is set to visible, because focus() does not work on invisible elements + // and the class definition is assigned later in the processing + setTimeout(() => { + if (focusOptions === undefined || focusOptions === null) { + this[inlineFilterElementSymbol].focus(); + } else { + this[inlineFilterElementSymbol].focus(focusOptions); + } + }, 100); } /** * @private */ function focusFilter(focusOptions) { - if (getFilterMode.call(this) === FILTER_MODE_DISABLED) { - this[popperFilterContainerElementSymbol].classList.add("d-none"); - this[inlineFilterElementSymbol].classList.add("d-none"); - return; - } + if (getFilterMode.call(this) === FILTER_MODE_DISABLED) { + this[popperFilterContainerElementSymbol].classList.add("d-none"); + this[inlineFilterElementSymbol].classList.add("d-none"); + return; + } - if (this.getOption("filter.position") === FILTER_POSITION_INLINE) { - return focusInlineFilter.call(this, focusOptions); - } + if (this.getOption("filter.position") === FILTER_POSITION_INLINE) { + return focusInlineFilter.call(this, focusOptions); + } - return focusPopperFilter.call(this, focusOptions); + return focusPopperFilter.call(this, focusOptions); } /** @@ -1629,40 +1625,39 @@ function focusFilter(focusOptions) { * @throws {Error} unsupported type */ function gatherState() { - const type = this.getOption("type"); - if (["radio", "checkbox"].indexOf(type) === -1) { - throw new Error("unsupported type"); - } - - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const selection = []; - const elements = this.shadowRoot.querySelectorAll( - `input[type=${type}]:checked`, - ); - - for (const e of elements) { - selection.push({ - label: getSelectionLabel.call(this, e.value), - value: e.value, - }); - } - - setSelection - .call(this, selection) - .then(() => { - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - - if (this.getOption("features.closeOnSelect") === true) { - toggle.call(this); - } - - return this; + const type = this.getOption("type"); + if (["radio", "checkbox"].indexOf(type) === -1) { + throw new Error("unsupported type"); + } + + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const selection = []; + const elements = this.shadowRoot.querySelectorAll( + `input[type=${type}]:checked`, + ); + + for (const e of elements) { + selection.push({ + label: getSelectionLabel.call(this, e.value), + value: e.value, + }); + } + + setSelection + .call(this, selection) + .then(() => {}) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + + if (this.getOption("features.closeOnSelect") === true) { + toggle.call(this); + } + + return this; } /** @@ -1671,121 +1666,120 @@ function gatherState() { * @throws {Error} unsupported type */ function clearSelection() { - const type = this.getOption("type"); - if (["radio", "checkbox"].indexOf(type) === -1) { - throw new Error("unsupported type"); - } - - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - setSelection - .call(this, []) - .then(() => { - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); + const type = this.getOption("type"); + if (["radio", "checkbox"].indexOf(type) === -1) { + throw new Error("unsupported type"); + } + + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + setSelection + .call(this, []) + .then(() => {}) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); } /** * @private */ function areOptionsAvailableAndInit() { - // prevent multiple calls - if (this[areOptionsAvailableAndInitSymbol] === undefined) { - this[areOptionsAvailableAndInitSymbol] = 0; - } - - if (this[areOptionsAvailableAndInitSymbol] > 0) { - this[areOptionsAvailableAndInitSymbol]--; - return true; - } - - this[areOptionsAvailableAndInitSymbol]++; - - const options = this.getOption("options"); - - if ( - options === undefined || - options === null || - (isArray(options) && options.length === 0) - ) { - setStatusOrRemoveBadges.call(this, "empty"); - - hide.call(this); - - let msg = this.getOption("labels.no-options-available"); - - if ( - this.getOption("url") !== null && - this.getOption("features.lazyLoad") === true && - this[lazyLoadDoneSymbol] !== true - ) { - msg = this.getOption("labels.click-to-load-options"); - } - - this.setOption("messages.control", msg); - this.setOption("messages.summary", ""); - - if (this.getOption("features.emptyValueIfNoOptions") === true) { - this.value = ""; - } - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage); - return false; - } - - const selections = this.getOption("selection"); - if ( - selections === undefined || - selections === null || - selections.length === 0 - ) { - this.setOption( - "messages.control", - this.getOption("labels.select-an-option"), - ); - } else { - this.setOption("messages.control", ""); - } - - this.setOption("messages.summary", setSummaryAndControlText.call(this)); - - let updated = false; - let valueCounter = 1; - for (const option of options) { - if (option?.visibility === undefined) { - option.visibility = "visible"; - updated = true; - } - - if (option?.value === undefined && option?.label === undefined) { - option.value = `${valueCounter++}`; - option.label = option.value; - updated = true; - continue; - } - - if (option?.value === undefined) { - option.value = option.label; - updated = true; - } - - if (option?.label === undefined) { - option.label = option.value; - updated = true; - } - } - - if (updated) { - this.setOption("options", options); - } - - setStatusOrRemoveBadges.call(this, "status"); - - removeAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage); - return true; + // prevent multiple calls + if (this[areOptionsAvailableAndInitSymbol] === undefined) { + this[areOptionsAvailableAndInitSymbol] = 0; + } + + if (this[areOptionsAvailableAndInitSymbol] > 0) { + this[areOptionsAvailableAndInitSymbol]--; + return true; + } + + this[areOptionsAvailableAndInitSymbol]++; + + const options = this.getOption("options"); + + if ( + options === undefined || + options === null || + (isArray(options) && options.length === 0) + ) { + setStatusOrRemoveBadges.call(this, "empty"); + + hide.call(this); + + let msg = this.getOption("labels.no-options-available"); + + if ( + this.getOption("url") !== null && + this.getOption("features.lazyLoad") === true && + this[lazyLoadDoneSymbol] !== true + ) { + msg = this.getOption("labels.click-to-load-options"); + } + + this.setOption("messages.control", msg); + this.setOption("messages.summary", ""); + + if (this.getOption("features.emptyValueIfNoOptions") === true) { + this.value = ""; + } + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage); + return false; + } + + const selections = this.getOption("selection"); + if ( + selections === undefined || + selections === null || + selections.length === 0 + ) { + this.setOption( + "messages.control", + this.getOption("labels.select-an-option"), + ); + } else { + this.setOption("messages.control", ""); + } + + this.setOption("messages.summary", setSummaryAndControlText.call(this)); + + let updated = false; + let valueCounter = 1; + for (const option of options) { + if (option?.visibility === undefined) { + option.visibility = "visible"; + updated = true; + } + + if (option?.value === undefined && option?.label === undefined) { + option.value = `${valueCounter++}`; + option.label = option.value; + updated = true; + continue; + } + + if (option?.value === undefined) { + option.value = option.label; + updated = true; + } + + if (option?.label === undefined) { + option.label = option.value; + updated = true; + } + } + + if (updated) { + this.setOption("options", options); + } + + setStatusOrRemoveBadges.call(this, "status"); + + removeAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, noOptionsAvailableMessage); + return true; } /** @@ -1793,30 +1787,30 @@ function areOptionsAvailableAndInit() { * @throws {Error} no shadow-root is defined */ function checkOptionState() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const elements = this.shadowRoot.querySelectorAll( - `[${ATTRIBUTE_ROLE}=option] input`, - ); - - let selection = this.getOption("selection"); - if (!isArray(selection)) { - selection = []; - } - - const checkedValues = selection.map((a) => { - return a.value; - }); - - for (const e of elements) { - if (checkedValues.indexOf(e.value) !== -1) { - if (e.checked !== true) e.checked = true; - } else { - if (e.checked !== false) e.checked = false; - } - } + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const elements = this.shadowRoot.querySelectorAll( + `[${ATTRIBUTE_ROLE}=option] input`, + ); + + let selection = this.getOption("selection"); + if (!isArray(selection)) { + selection = []; + } + + const checkedValues = selection.map((a) => { + return a.value; + }); + + for (const e of elements) { + if (checkedValues.indexOf(e.value) !== -1) { + if (e.checked !== true) e.checked = true; + } else { + if (e.checked !== false) e.checked = false; + } + } } /** @@ -1825,41 +1819,41 @@ function checkOptionState() { * @return {Object} */ function convertValueToSelection(value) { - const selection = []; - - if (isString(value)) { - value = value - .split(",") - .map((a) => { - return a.trim(); - }) - .filter((a) => { - return a !== ""; - }); - } - - if (isString(value) || isInteger(value)) { - selection.push({ - label: getSelectionLabel.call(this, value), - value: value, - }); - } else if (isArray(value)) { - for (const v of value) { - selection.push({ - label: getSelectionLabel.call(this, v), - value: v, - }); - } - - value = value.join(","); - } else { - throw new Error("unsupported type"); - } - - return { - selection: selection, - value: value, - }; + const selection = []; + + if (isString(value)) { + value = value + .split(",") + .map((a) => { + return a.trim(); + }) + .filter((a) => { + return a !== ""; + }); + } + + if (isString(value) || isInteger(value)) { + selection.push({ + label: getSelectionLabel.call(this, value), + value: value, + }); + } else if (isArray(value)) { + for (const v of value) { + selection.push({ + label: getSelectionLabel.call(this, v), + value: v, + }); + } + + value = value.join(","); + } else { + throw new Error("unsupported type"); + } + + return { + selection: selection, + value: value, + }; } /** @@ -1868,22 +1862,22 @@ function convertValueToSelection(value) { * @return {string} */ function convertSelectionToValue(selection) { - const value = []; - - if (isArray(selection)) { - for (const obj of selection) { - const v = obj?.["value"]; - if (v !== undefined) value.push(v); - } - } - - if (value.length === 0) { - return ""; - } else if (value.length === 1) { - return value.pop(); - } - - return value.join(","); + const value = []; + + if (isArray(selection)) { + for (const obj of selection) { + const v = obj?.["value"]; + if (v !== undefined) value.push(v); + } + } + + if (value.length === 0) { + return ""; + } else if (value.length === 1) { + return value.pop(); + } + + return value.join(","); } /** @@ -1893,57 +1887,58 @@ function convertSelectionToValue(selection) { * @throws {Error} no shadow-root is defined */ function setSelection(selection) { - - if (isString(selection)) { - const result = convertValueToSelection.call(this, selection); - selection = result?.selection; - } else if (selection === undefined) { - selection = []; - } - - this.setOption("selection", validateArray(selection)); - checkOptionState.call(this); - - try { - this?.setFormValue(this.value); - } catch (e) { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - } - - fireCustomEvent(this, "monster-selected", { - selection, - }); - - return new Processing(() => { - const CLASSNAME = "selected"; - - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const notSelected = this.shadowRoot.querySelectorAll(":not(:checked)"); - - if (notSelected) { - notSelected.forEach((node) => { - const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`); - if (parent) { - parent.classList.remove(CLASSNAME); - } - }); - } - - const selected = this.shadowRoot.querySelectorAll(":checked"); - if (selected) { - selected.forEach((node) => { - const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`); - if (parent) { - parent.classList.add(CLASSNAME); - } - }); - } - }).run().catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); + if (isString(selection)) { + const result = convertValueToSelection.call(this, selection); + selection = result?.selection; + } else if (selection === undefined) { + selection = []; + } + + this.setOption("selection", validateArray(selection)); + checkOptionState.call(this); + + try { + this?.setFormValue(this.value); + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + } + + fireCustomEvent(this, "monster-selected", { + selection, + }); + + return new Processing(() => { + const CLASSNAME = "selected"; + + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const notSelected = this.shadowRoot.querySelectorAll(":not(:checked)"); + + if (notSelected) { + notSelected.forEach((node) => { + const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`); + if (parent) { + parent.classList.remove(CLASSNAME); + } + }); + } + + const selected = this.shadowRoot.querySelectorAll(":checked"); + if (selected) { + selected.forEach((node) => { + const parent = node.closest(`[${ATTRIBUTE_ROLE}=option]`); + if (parent) { + parent.classList.add(CLASSNAME); + } + }); + } + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); } /** @@ -1954,136 +1949,134 @@ function setSelection(selection) { * @throws {TypeError} unsupported response */ function fetchData(url) { - - if (!url) url = this.getOption("url"); - if (!url) return Promise.resolve(); - - const fetchOptions = this.getOption("fetch", {}); - - setStatusOrRemoveBadges.call(this, "loading"); - url = new Formatter({filter: this.getOption("filter.defaultValue")}).format( - url, - ); - - const global = getGlobal(); - return global - .fetch(url, fetchOptions) - .then((response) => { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return response.text(); - } - - throw new TypeError(`unsupported response ${contentType}`); - }) - .then((text) => { - try { - return Promise.resolve(JSON.parse(String(text))); - } catch (e) { - throw new TypeError("the result cannot be parsed"); - } - }) - .catch((e) => { - throw e; - }); + if (!url) url = this.getOption("url"); + if (!url) return Promise.resolve(); + + const fetchOptions = this.getOption("fetch", {}); + + setStatusOrRemoveBadges.call(this, "loading"); + url = new Formatter({ filter: this.getOption("filter.defaultValue") }).format( + url, + ); + + const global = getGlobal(); + return global + .fetch(url, fetchOptions) + .then((response) => { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return response.text(); + } + + throw new TypeError(`unsupported response ${contentType}`); + }) + .then((text) => { + try { + return Promise.resolve(JSON.parse(String(text))); + } catch (e) { + throw new TypeError("the result cannot be parsed"); + } + }) + .catch((e) => { + throw e; + }); } /** * @private */ function hide() { - this[popperElementSymbol].style.display = "none"; - setStatusOrRemoveBadges.call(this, "status"); - removeAttributeToken(this[controlElementSymbol], "class", "open"); + this[popperElementSymbol].style.display = "none"; + setStatusOrRemoveBadges.call(this, "status"); + removeAttributeToken(this[controlElementSymbol], "class", "open"); } /** * @private */ function show() { - - if (this.getOption("disabled", undefined) === true) { - return; - } - - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - return; - } - - focusFilter.call(this); - - const lazyLoadFlag = - this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true; - - if (lazyLoadFlag === true) { - this[lazyLoadDoneSymbol] = true; - setStatusOrRemoveBadges.call(this, "loading"); - - new Processing(200, () => { - this.fetch() - .then(() => { - setTimeout(() => { - let result; - if (this.hasAttribute("value")) { - result = setSelection.call(this, this.getAttribute("value")); - } else { - result = setSelection.call(this, []); - } - - result - .then(() => { - show.call(this); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); - }); - }, 100); - }) - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - setStatusOrRemoveBadges.call(this, "error"); - }); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - - return; - } - - const options = getOptionElements.call(this); - if (options.length === 0) { - return; - } - - this[popperElementSymbol].style.visibility = "hidden"; - this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; - setStatusOrRemoveBadges.call(this, "open"); - - addAttributeToken(this[controlElementSymbol], "class", "open"); - - new Processing(() => { - calcAndSetOptionsDimension.call(this); - focusFilter.call(this); - this[popperElementSymbol].style.removeProperty("visibility"); - updatePopper.call(this); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); + if (this.getOption("disabled", undefined) === true) { + return; + } + + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + return; + } + + focusFilter.call(this); + + const lazyLoadFlag = + this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true; + + if (lazyLoadFlag === true) { + this[lazyLoadDoneSymbol] = true; + setStatusOrRemoveBadges.call(this, "loading"); + + new Processing(200, () => { + this.fetch() + .then(() => { + setTimeout(() => { + let result; + if (this.hasAttribute("value")) { + result = setSelection.call(this, this.getAttribute("value")); + } else { + result = setSelection.call(this, []); + } + + result + .then(() => { + show.call(this); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`); + }); + }, 100); + }) + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + setStatusOrRemoveBadges.call(this, "error"); + }); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + + return; + } + + const options = getOptionElements.call(this); + if (options.length === 0) { + return; + } + + this[popperElementSymbol].style.visibility = "hidden"; + this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; + setStatusOrRemoveBadges.call(this, "open"); + + addAttributeToken(this[controlElementSymbol], "class", "open"); + + new Processing(() => { + calcAndSetOptionsDimension.call(this); + focusFilter.call(this); + this[popperElementSymbol].style.removeProperty("visibility"); + updatePopper.call(this); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); } /** * @private */ function toggle() { - if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { - hide.call(this); - } else { - show.call(this); - } + if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { + hide.call(this); + } else { + show.call(this); + } } /** @@ -2092,188 +2085,188 @@ function toggle() { * @fires Monster.Components.Form.event:monster-selection-cleared */ function initEventHandler() { - const self = this; - - /** - * @param {Event} event - */ - self[clearOptionEventHandler] = (event) => { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "remove-badge", - ); - - if (element instanceof HTMLElement) { - const badge = findClosestByAttribute(element, ATTRIBUTE_ROLE, "badge"); - if (badge instanceof HTMLElement) { - const value = badge.getAttribute(`${ATTRIBUTE_PREFIX}value`); - - let selection = self.getOption("selection"); - selection = selection.filter((b) => { - return value !== b.value; - }); - - setSelection - .call(self, selection) - .then(() => { - fireCustomEvent(self, "monster-selection-removed", { - value, - }); - }) - .catch((e) => { - addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - } - } - }; - - /** - * @param {Event} event - */ - self[closeEventHandler] = (event) => { - const path = event.composedPath(); - - for (const [, element] of Object.entries(path)) { - if (element === self) { - return; - } - } - hide.call(self); - }; - - /** - * @param {Event} event - */ - self[inputEventHandler] = (event) => { - const path = event.composedPath(); - const element = path?.[0]; - - if (element instanceof HTMLElement) { - if ( - element.hasAttribute(ATTRIBUTE_ROLE) && - element.getAttribute(ATTRIBUTE_ROLE) === "option-control" - ) { - fireCustomEvent(self, "monster-change", { - type: event.type, - value: element.value, - checked: element.checked, - }); - } else if ( - element.hasAttribute(ATTRIBUTE_ROLE) && - element.getAttribute(ATTRIBUTE_ROLE) === "filter" - ) { - } - } - }; - - /** - * @param {Event} event - */ - self[changeEventHandler] = (event) => { - gatherState.call(self); - fireCustomEvent(self, "monster-changed", event?.detail); - }; - - self[keyEventHandler] = (event) => { - const path = event.composedPath(); - const element = path.shift(); - - let role; - - if (element instanceof HTMLElement) { - if (element.hasAttribute(ATTRIBUTE_ROLE)) { - role = element.getAttribute(ATTRIBUTE_ROLE); - } else if (element === this) { - show.call(this); - // focusFilter.call(self); - } else { - const e = element.closest(`[${ATTRIBUTE_ROLE}]`); - if (e instanceof HTMLElement && e.hasAttribute(ATTRIBUTE_ROLE)) { - role = e.getAttribute(ATTRIBUTE_ROLE); - } - } - } else { - return; - } - - switch (role) { - case "filter": - handleFilterKeyboardEvents.call(self, event); - break; - case "option-label": - case "option-control": - case "option": - handleOptionKeyboardEvents.call(self, event); - break; - case "control": - case "toggle": - handleToggleKeyboardEvents.call(self, event); - break; - } - }; - - const types = self.getOption("toggleEventType", ["click"]); - - for (const [, type] of Object.entries(types)) { - self[controlElementSymbol] - .querySelector(`[${ATTRIBUTE_ROLE}="container"]`) - .addEventListener(type, function (event) { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "remove-badge", - ); - if (element instanceof HTMLElement) { - return; - } - - toggle.call(self); - }); - - self[controlElementSymbol] - .querySelector(`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`) - .addEventListener(type, function (event) { - if (self.getOption("disabled", undefined) === true) { - return; - } - - const path = event.composedPath(); - const element = path?.[0]; - if (element instanceof HTMLElement) { - const control = element.closest( - `[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`, - ); - if (control instanceof HTMLElement) { - if (control.classList.contains("clear")) { - clearSelection.call(self); - - fireCustomEvent(self, "monster-selection-cleared", {}); - } else { - const element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "remove-badge", - ); - if (element instanceof HTMLElement) { - return; - } - - toggle.call(self); - } - } - } - }); - - // badge, selection - self.addEventListener(type, self[clearOptionEventHandler]); - } - - self.addEventListener("monster-change", self[changeEventHandler]); - self.addEventListener("input", self[inputEventHandler]); - self.addEventListener("keydown", self[keyEventHandler]); - - return self; + const self = this; + + /** + * @param {Event} event + */ + self[clearOptionEventHandler] = (event) => { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "remove-badge", + ); + + if (element instanceof HTMLElement) { + const badge = findClosestByAttribute(element, ATTRIBUTE_ROLE, "badge"); + if (badge instanceof HTMLElement) { + const value = badge.getAttribute(`${ATTRIBUTE_PREFIX}value`); + + let selection = self.getOption("selection"); + selection = selection.filter((b) => { + return value !== b.value; + }); + + setSelection + .call(self, selection) + .then(() => { + fireCustomEvent(self, "monster-selection-removed", { + value, + }); + }) + .catch((e) => { + addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + } + } + }; + + /** + * @param {Event} event + */ + self[closeEventHandler] = (event) => { + const path = event.composedPath(); + + for (const [, element] of Object.entries(path)) { + if (element === self) { + return; + } + } + hide.call(self); + }; + + /** + * @param {Event} event + */ + self[inputEventHandler] = (event) => { + const path = event.composedPath(); + const element = path?.[0]; + + if (element instanceof HTMLElement) { + if ( + element.hasAttribute(ATTRIBUTE_ROLE) && + element.getAttribute(ATTRIBUTE_ROLE) === "option-control" + ) { + fireCustomEvent(self, "monster-change", { + type: event.type, + value: element.value, + checked: element.checked, + }); + } else if ( + element.hasAttribute(ATTRIBUTE_ROLE) && + element.getAttribute(ATTRIBUTE_ROLE) === "filter" + ) { + } + } + }; + + /** + * @param {Event} event + */ + self[changeEventHandler] = (event) => { + gatherState.call(self); + fireCustomEvent(self, "monster-changed", event?.detail); + }; + + self[keyEventHandler] = (event) => { + const path = event.composedPath(); + const element = path.shift(); + + let role; + + if (element instanceof HTMLElement) { + if (element.hasAttribute(ATTRIBUTE_ROLE)) { + role = element.getAttribute(ATTRIBUTE_ROLE); + } else if (element === this) { + show.call(this); + // focusFilter.call(self); + } else { + const e = element.closest(`[${ATTRIBUTE_ROLE}]`); + if (e instanceof HTMLElement && e.hasAttribute(ATTRIBUTE_ROLE)) { + role = e.getAttribute(ATTRIBUTE_ROLE); + } + } + } else { + return; + } + + switch (role) { + case "filter": + handleFilterKeyboardEvents.call(self, event); + break; + case "option-label": + case "option-control": + case "option": + handleOptionKeyboardEvents.call(self, event); + break; + case "control": + case "toggle": + handleToggleKeyboardEvents.call(self, event); + break; + } + }; + + const types = self.getOption("toggleEventType", ["click"]); + + for (const [, type] of Object.entries(types)) { + self[controlElementSymbol] + .querySelector(`[${ATTRIBUTE_ROLE}="container"]`) + .addEventListener(type, function (event) { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "remove-badge", + ); + if (element instanceof HTMLElement) { + return; + } + + toggle.call(self); + }); + + self[controlElementSymbol] + .querySelector(`[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`) + .addEventListener(type, function (event) { + if (self.getOption("disabled", undefined) === true) { + return; + } + + const path = event.composedPath(); + const element = path?.[0]; + if (element instanceof HTMLElement) { + const control = element.closest( + `[${ATTRIBUTE_ROLE}="status-or-remove-badges"]`, + ); + if (control instanceof HTMLElement) { + if (control.classList.contains("clear")) { + clearSelection.call(self); + + fireCustomEvent(self, "monster-selection-cleared", {}); + } else { + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "remove-badge", + ); + if (element instanceof HTMLElement) { + return; + } + + toggle.call(self); + } + } + } + }); + + // badge, selection + self.addEventListener(type, self[clearOptionEventHandler]); + } + + self.addEventListener("monster-change", self[changeEventHandler]); + self.addEventListener("input", self[inputEventHandler]); + self.addEventListener("keydown", self[keyEventHandler]); + + return self; } /** @@ -2281,56 +2274,55 @@ function initEventHandler() { * @return {Select} */ function setStatusOrRemoveBadges(suggestion) { - - setTimeout(() => { - const selection = this.getOption("selection"); - const clearAllFlag = - isArray(selection) && - selection.length > 0 && - this.getOption("features.clearAll") === true; - - const current = this.getOption("classes.statusOrRemoveBadge"); - - if (clearAllFlag) { - if (current !== "clear") { - this.setOption("classes.statusOrRemoveBadge", "clear"); - } - return; - } - - if (suggestion === "loading") { - if (current !== "loading") { - this.setOption("classes.statusOrRemoveBadge", "loading"); - } - return; - } - - if (this[controlElementSymbol].classList.contains("open")) { - if (current !== "open") { - this.setOption("classes.statusOrRemoveBadge", "open"); - } - return; - } - - const options = this.getOption("options"); - if ( - options === undefined || - options === null || - (isArray(options) && options.length === 0) - ) { - if (current !== "empty") { - this.setOption("classes.statusOrRemoveBadge", "empty"); - } - return; - } - - if (suggestion) { - if (current !== suggestion) { - this.setOption("classes.statusOrRemoveBadge", suggestion); - } - return; - } - }, 2); + setTimeout(() => { + const selection = this.getOption("selection"); + const clearAllFlag = + isArray(selection) && + selection.length > 0 && + this.getOption("features.clearAll") === true; + + const current = this.getOption("classes.statusOrRemoveBadge"); + + if (clearAllFlag) { + if (current !== "clear") { + this.setOption("classes.statusOrRemoveBadge", "clear"); + } + return; + } + + if (suggestion === "loading") { + if (current !== "loading") { + this.setOption("classes.statusOrRemoveBadge", "loading"); + } + return; + } + + if (this[controlElementSymbol].classList.contains("open")) { + if (current !== "open") { + this.setOption("classes.statusOrRemoveBadge", "open"); + } + return; + } + + const options = this.getOption("options"); + if ( + options === undefined || + options === null || + (isArray(options) && options.length === 0) + ) { + if (current !== "empty") { + this.setOption("classes.statusOrRemoveBadge", "empty"); + } + return; + } + + if (suggestion) { + if (current !== suggestion) { + this.setOption("classes.statusOrRemoveBadge", suggestion); + } + return; + } + }, 2); } /** @@ -2339,68 +2331,68 @@ function setStatusOrRemoveBadges(suggestion) { * @throws {Error} no shadow-root is defined */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - this[controlElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=control]`, - ); - this[selectionElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=selection]`, - ); - this[containerElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=container]`, - ); - this[popperElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper]`, - ); - this[inlineFilterElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=filter][name="inline-filter"]`, - ); - this[popperFilterElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=filter][name="popper-filter"]`, - ); - this[popperFilterContainerElementSymbol] = - this[popperFilterElementSymbol].parentElement; - this[optionsElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=options]`, - ); - this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}="no-options"]`, - ); - this[statusOrRemoveBadgesElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=status-or-remove-badges]`, - ); + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + this[controlElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=control]`, + ); + this[selectionElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=selection]`, + ); + this[containerElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=container]`, + ); + this[popperElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper]`, + ); + this[inlineFilterElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=filter][name="inline-filter"]`, + ); + this[popperFilterElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=filter][name="popper-filter"]`, + ); + this[popperFilterContainerElementSymbol] = + this[popperFilterElementSymbol].parentElement; + this[optionsElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=options]`, + ); + this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}="no-options"]`, + ); + this[statusOrRemoveBadgesElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=status-or-remove-badges]`, + ); } /** * @private */ function updatePopper() { - if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { - return; - } - - if (this.getOption("disabled", false) === true) { - return; - } - - new Processing(() => { - calcAndSetOptionsDimension.call(this); - positionPopper.call( - this, - this[controlElementSymbol], - this[popperElementSymbol], - this.getOption("popper", {}), - ); - }) - .run() - .catch((e) => { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - }); - - return this; + if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { + return; + } + + if (this.getOption("disabled", false) === true) { + return; + } + + new Processing(() => { + calcAndSetOptionsDimension.call(this); + positionPopper.call( + this, + this[controlElementSymbol], + this[popperElementSymbol], + this.getOption("popper", {}), + ); + }) + .run() + .catch((e) => { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + }); + + return this; } /** @@ -2408,8 +2400,8 @@ function updatePopper() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <template id="options"> <div data-monster-role="option" tabindex="-1" data-monster-attributes=" diff --git a/source/components/form/stylesheet/select.mjs b/source/components/form/stylesheet/select.mjs index 3914f61ba5a76b5b1856077782e80182beccc5cb..c839030524f45c480102a66868b6d9d553a00cf5 100644 --- a/source/components/form/stylesheet/select.mjs +++ b/source/components/form/stylesheet/select.mjs @@ -20,7 +20,7 @@ const SelectStyleSheet = new CSSStyleSheet(); try { SelectStyleSheet.insertRule(` @layer select { -.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-border-primary-1,.monster-border-primary-2,.monster-border-primary-3,.monster-border-primary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-0{border-radius:0;border-style:none;border-width:0;border-shadow:none}.monster-border-primary-1{border-color:var(--monster-bg-color-primary-1)}.monster-border-primary-2{border-color:var(--monster-bg-color-primary-2)}.monster-border-primary-3{border-color:var(--monster-bg-color-primary-3)}.monster-border-primary-4{border-color:var(--monster-bg-color-primary-4)}.monster-border-secondary-1,.monster-border-secondary-2,.monster-border-secondary-3,.monster-border-secondary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-secondary-1{border-color:var(--monster-bg-color-secondary-1)}.monster-border-secondary-2{border-color:var(--monster-bg-color-secondary-2)}.monster-border-secondary-3{border-color:var(--monster-bg-color-secondary-3)}.monster-border-secondary-4{border-color:var(--monster-bg-color-secondary-4)}.monster-border-tertiary-1,.monster-border-tertiary-2,.monster-border-tertiary-3,.monster-border-tertiary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-tertiary-1{border-color:var(--monster-bg-color-tertiary-1)}.monster-border-tertiary-2{border-color:var(--monster-bg-color-tertiary-2)}.monster-border-tertiary-3{border-color:var(--monster-bg-color-tertiary-3)}.monster-border-tertiary-4{border-color:var(--monster-bg-color-tertiary-4)}[data-monster-role=control]{width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}.monster-badge-primary{padding:.25em .4em}.monster-badge-primary,.monster-badge-primary-pill{background-color:var(--monster-bg-color-primary-4);border-radius:.25rem;color:var(--monster-color-primary-4);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-primary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-secondary{padding:.25em .4em}.monster-badge-secondary,.monster-badge-secondary-pill{background-color:var(--monster-bg-color-secondary-3);border-radius:.25rem;color:var(--monster-color-secondary-3);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-secondary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-tertiary{padding:.25em .4em}.monster-badge-tertiary,.monster-badge-tertiary-pill{background-color:var(--monster-bg-color-tertiary-3);border-radius:.25rem;color:var(--monster-color-tertiary-3);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-tertiary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-destructive{padding:.25em .4em}.monster-badge-destructive,.monster-badge-destructive-pill{background-color:var(--monster-bg-color-destructive-1);border-radius:.25rem;color:var(--monster-color-destructive-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-destructive-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-success{padding:.25em .4em}.monster-badge-success,.monster-badge-success-pill{background-color:var(--monster-bg-color-success-1);border-radius:.25rem;color:var(--monster-color-success-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-success-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-warning{padding:.25em .4em}.monster-badge-warning,.monster-badge-warning-pill{background-color:var(--monster-bg-color-warning-1);border-radius:.25rem;color:var(--monster-color-warning-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-warning-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-error{padding:.25em .4em}.monster-badge-error,.monster-badge-error-pill{background-color:var(--monster-bg-color-error-1);border-radius:.25rem;color:var(--monster-color-error-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-error-pill{border-radius:10rem;padding:.25em .6em}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=container]{-webkit-appearance:none;-moz-appearance:none;appearance:none;flex:4 0 90%;min-height:1.4em}.d-none{display:none!important}div[data-monster-role=control]{display:flex;height:100%;position:relative}[data-monster-role=container].open{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\")}[data-monster-role=container].clear{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\")}[data-monster-role=control]{accent-color:var(--monster-color-secondary-2);background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-2);-o-border-image:initial;border-image:initial;border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:thin;box-sizing:border-box;color:var(--monster-color-primary-2);display:flex;font-family:inherit;font-size:100%;margin:0;outline:none;padding:.4rem .6rem}@media (prefers-color-scheme:light){[data-monster-role=control]{background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2)}[data-monster-role=control]:focus{outline:1px dashed var(--monster-color-selection-3);outline-offset:2px}}[data-monster-role=control]:hover{box-shadow:var(--monster-box-shadow-2);transition:background .8s,color .25s .0833333333s}div[data-monster-role=selection]{align-items:center;display:flex;flex-direction:row;flex-wrap:wrap;gap:5px;justify-content:flex-start;margin:5px}[data-monster-role=option-control]{margin-right:8px}[data-monster-role=badge]{display:inline-flex}[data-monster-role=badge-label]{align-content:center;align-items:center;display:flex;flex-direction:row;gap:7px;justify-content:space-between}[data-monster-role=remove-badge]{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-position:100% 100%;background-repeat:no-repeat;background-size:16px;height:16px;margin-left:5px;min-height:16px;order:2;width:16px}@media (prefers-color-scheme:light){[data-monster-role=remove-badge]{filter:invert(1)}}[data-monster-role=filter],[data-monster-role=popper-filter]{display:flex;flex-grow:200;order:99999999;visibility:hidden}[data-monster-role=filter].active{background-color:var(--monster-bg-color-primary-2);border:0;border-color:var(--monster-bg-color-primary-3);border-bottom:1px solid var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2);min-width:40%;outline:none;visibility:visible}.active[data-monster-role=filter][name=popper-filter]{height:1.5em;margin:2.5em;padding:2px;width:calc(100% - var(--monster-border-width)*2)}.option-filter-control{align-items:center;background-color:var(--monster-bg-color-primary-2);display:flex;height:2.5em;margin:-1.1em -1.1em .3em}.status-or-remove-badges{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-position:50%;background-repeat:no-repeat;background-size:16px;min-height:16px;width:16px}[data-monster-role=status-or-remove-badges].open{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].empty{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].clear{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].loading{animation-duration:1s;animation-iteration-count:infinite;animation-name:activity;animation-timing-function:cubic-bezier(0,0,.2,1);background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\")}@keyframes activity{0%{transform:scale(1)}50%{transform:scale(.1)}to{transform:scale(1)}}[data-monster-role=options]{box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1}[data-monster-role=option]{align-items:center;box-sizing:border-box;display:flex;padding:6px 5px}[data-monster-role=option] label{align-items:center;display:flex;flex-direction:row;justify-content:flex-start;width:100%}[data-monster-role=option] label>div{align-items:center;display:flex;flex-direction:row;justify-content:space-between;outline:none;width:100%}[data-monster-role=no-options]{background-color:var(--monster-bg-color-warning-2);color:var(--monster-col or-warning-2);margin:1.1em 0 0 1.1em}.selected{background-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-3)}[data-monster-role=option][data-monster-filtered=true],[data-monster-role=option][data-monster-visibility=hidden]{display:none}[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-2);outline-offset:-2px}[data-monster-role=option]>input:focus,[data-monster-role=option]>label:focus{outline:none} +.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-border-primary-1,.monster-border-primary-2,.monster-border-primary-3,.monster-border-primary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-0{border-radius:0;border-style:none;border-width:0;border-shadow:none}.monster-border-primary-1{border-color:var(--monster-bg-color-primary-1)}.monster-border-primary-2{border-color:var(--monster-bg-color-primary-2)}.monster-border-primary-3{border-color:var(--monster-bg-color-primary-3)}.monster-border-primary-4{border-color:var(--monster-bg-color-primary-4)}.monster-border-secondary-1,.monster-border-secondary-2,.monster-border-secondary-3,.monster-border-secondary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-secondary-1{border-color:var(--monster-bg-color-secondary-1)}.monster-border-secondary-2{border-color:var(--monster-bg-color-secondary-2)}.monster-border-secondary-3{border-color:var(--monster-bg-color-secondary-3)}.monster-border-secondary-4{border-color:var(--monster-bg-color-secondary-4)}.monster-border-tertiary-1,.monster-border-tertiary-2,.monster-border-tertiary-3,.monster-border-tertiary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);border-shadow:var(--monster-box-shadow-1)}.monster-border-tertiary-1{border-color:var(--monster-bg-color-tertiary-1)}.monster-border-tertiary-2{border-color:var(--monster-bg-color-tertiary-2)}.monster-border-tertiary-3{border-color:var(--monster-bg-color-tertiary-3)}.monster-border-tertiary-4{border-color:var(--monster-bg-color-tertiary-4)}[data-monster-role=control]{width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}.monster-badge-primary{padding:.25em .4em}.monster-badge-primary,.monster-badge-primary-pill{background-color:var(--monster-bg-color-primary-4);border-radius:.25rem;color:var(--monster-color-primary-4);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-primary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-secondary{padding:.25em .4em}.monster-badge-secondary,.monster-badge-secondary-pill{background-color:var(--monster-bg-color-secondary-3);border-radius:.25rem;color:var(--monster-color-secondary-3);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-secondary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-tertiary{padding:.25em .4em}.monster-badge-tertiary,.monster-badge-tertiary-pill{background-color:var(--monster-bg-color-tertiary-3);border-radius:.25rem;color:var(--monster-color-tertiary-3);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-tertiary-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-destructive{padding:.25em .4em}.monster-badge-destructive,.monster-badge-destructive-pill{background-color:var(--monster-bg-color-destructive-1);border-radius:.25rem;color:var(--monster-color-destructive-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-destructive-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-success{padding:.25em .4em}.monster-badge-success,.monster-badge-success-pill{background-color:var(--monster-bg-color-success-1);border-radius:.25rem;color:var(--monster-color-success-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-success-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-warning{padding:.25em .4em}.monster-badge-warning,.monster-badge-warning-pill{background-color:var(--monster-bg-color-warning-1);border-radius:.25rem;color:var(--monster-color-warning-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-warning-pill{border-radius:10rem;padding:.25em .6em}.monster-badge-error{padding:.25em .4em}.monster-badge-error,.monster-badge-error-pill{background-color:var(--monster-bg-color-error-1);border-radius:.25rem;color:var(--monster-color-error-1);display:inline-block;font-size:75%;font-weight:700;line-height:1;text-align:center;text-decoration:none;vertical-align:baseline;white-space:nowrap}.monster-badge-error-pill{border-radius:10rem;padding:.25em .6em}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=container]{-webkit-appearance:none;-moz-appearance:none;appearance:none;flex:4 0 90%;min-height:1.4em}.d-none{display:none!important}div[data-monster-role=control]{display:flex;height:100%;position:relative}[data-monster-role=container].open{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\")}[data-monster-role=container].clear{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\")}[data-monster-role=control]{accent-color:var(--monster-color-secondary-2);background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-2);-o-border-image:initial;border-image:initial;border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:thin;box-sizing:border-box;color:var(--monster-color-primary-2);display:flex;font-family:inherit;font-size:100%;margin:0;outline:none;padding:.4rem .6rem}@media (prefers-color-scheme:light){[data-monster-role=control]{background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2)}[data-monster-role=control]:focus{outline:1px dashed var(--monster-color-selection-3);outline-offset:2px}}[data-monster-role=control]:hover{box-shadow:var(--monster-box-shadow-2);transition:background .8s,color .25s .0833333333s}div[data-monster-role=selection]{align-items:center;display:flex;flex-direction:row;flex-wrap:wrap;gap:5px;justify-content:flex-start;margin:5px}[data-monster-role=option-control]{margin-right:8px}[data-monster-role=badge]{display:inline-flex}[data-monster-role=badge-label]{align-content:center;align-items:center;display:flex;flex-direction:row;gap:7px;justify-content:space-between}[data-monster-role=remove-badge]{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-position:100% 100%;background-repeat:no-repeat;background-size:16px;height:16px;margin-left:5px;min-height:16px;order:2;width:16px}@media (prefers-color-scheme:light){[data-monster-role=remove-badge]{filter:invert(1)}}[data-monster-role=filter],[data-monster-role=popper-filter]{display:flex;flex-grow:200;order:99999999;visibility:hidden}[data-monster-role=filter].active{background-color:var(--monster-bg-color-primary-2);border:0;border-color:var(--monster-bg-color-primary-3);border-bottom:1px solid var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2);min-width:40%;outline:none;visibility:visible}.active[data-monster-role=filter][name=popper-filter]{height:1.5em;margin:2.5em;padding:2px;width:calc(100% - var(--monster-border-width)*2)}.option-filter-control{align-items:center;background-color:var(--monster-bg-color-primary-2);display:flex;height:2.5em;margin:-1.1em -1.1em .3em}.status-or-remove-badges{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-position:50%;background-repeat:no-repeat;background-size:16px;min-height:16px;width:16px}[data-monster-role=status-or-remove-badges].open{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].empty{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].clear{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\")}[data-monster-role=status-or-remove-badges].loading{animation-duration:1s;animation-iteration-count:infinite;animation-name:activity;animation-timing-function:cubic-bezier(0,0,.2,1);background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\")}@keyframes activity{0%{transform:scale(1)}50%{transform:scale(.1)}to{transform:scale(1)}}[data-monster-role=options]{box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1}[data-monster-role=option]{align-items:center;box-sizing:border-box;display:flex;padding:6px 5px}[data-monster-role=option] label{align-items:center;display:flex;flex-direction:row;justify-content:flex-start;width:100%}[data-monster-role=option] label>div{align-items:center;display:flex;flex-direction:row;justify-content:space-between;outline:none;width:100%}[data-monster-role=no-options]{background-color:var(--monster-bg-color-warning-2);color:var(--monster-color-warning-2);margin:1.1em 0 0 1.1em}.selected{background-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-3)}[data-monster-role=option][data-monster-filtered=true],[data-monster-role=option][data-monster-visibility=hidden]{display:none}[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-2);outline-offset:-2px}[data-monster-role=option]>input:focus,[data-monster-role=option]>label:focus{outline:none} }`, 0); } catch (e) { addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + ""); diff --git a/source/components/form/tabs.mjs b/source/components/form/tabs.mjs index f119da223f19e1e2d7ffe0c11de68967a1943917..1c7faaaf8a396f539884e831b514743ebd37780a 100644 --- a/source/components/form/tabs.mjs +++ b/source/components/form/tabs.mjs @@ -594,7 +594,6 @@ function show(element) { * @private */ function initEventHandler() { - if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } diff --git a/source/components/form/toggle-switch.mjs b/source/components/form/toggle-switch.mjs index aa2a4a90c28c11fc46e3898af2a110a6b0c8088d..299a07a716664af8d4de1106475922a3e6931fdf 100644 --- a/source/components/form/toggle-switch.mjs +++ b/source/components/form/toggle-switch.mjs @@ -12,17 +12,15 @@ import { ProxyObserver } from "../../types/proxyobserver.mjs"; import { addAttributeToken } from "../../dom/attributes.mjs"; import { - assembleMethodSymbol, - registerCustomElement, - updaterTransformerMethodsSymbol + assembleMethodSymbol, + registerCustomElement, + updaterTransformerMethodsSymbol, } from "../../dom/customelement.mjs"; -import { - isObject -} from "../../types/is.mjs"; +import { isObject } from "../../types/is.mjs"; import { ToggleSwitchStyleSheet } from "./stylesheet/toggle-switch.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_ROLE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; export { ToggleSwitch }; @@ -47,18 +45,18 @@ const switchElementSymbolOff = Symbol("switchElementOff"); /** * @type {string} */ -export const STATE_ON = 'on'; +export const STATE_ON = "on"; /** * @type {string} */ -export const STATE_OFF = 'off'; +export const STATE_OFF = "off"; /** * This CustomControl creates a ToggleSwitch element * * <img src="./images/switch.png"> - * + * * * @startuml toggleswitch.png * skinparam monochrome true @@ -67,215 +65,217 @@ export const STATE_OFF = 'off'; * CustomElement <|-- CustomControl * CustomControl <|-- ToggleSwitch * @enduml - * - * @since 3.57.0 + * + * @since 3.57.0 * @copyright schukai GmbH * @memberOf Monster.Components.Form * @summary A simple Switch */ class ToggleSwitch extends CustomControl { + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for( + "@schukai/monster/components/form/toggle-switch@@instance", + ); + } + + static getTag() { + return "monster-toggle-switch"; + } + + /** + * To set the options via the html tag the attribute `data-monster-options` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {string} value=current value of the element + * @property {Boolean} disabled=disabled=false Disabled state + * @property {Object} classes + * @property {string} classes.on=specifies the class for the on state. + * @property {string} classes.off=specifies the class for the off state. + * @property {Object} values + * @property {string} values.off=specifies the value of the element if it is not selected + * @property {Object} labels + * @property {string} labels.on=specifies the label for the on state. + * @property {string} labels.off=specifies the label for the off state. + * @property {Object} templates + * @property {string} templates.main=specifies the main template used by the control. + * + * @since 3.57.0 + */ + get defaults() { + return Object.assign({}, super.defaults, { + value: null, + disabled: false, + classes: { + on: "monster-theme-primary-3", + off: "monster-theme-primary-2", + }, + values: { + on: "on", + off: "off", + }, + labels: { + "toggle-switch-on": "ON", + "toggle-switch-off": "OFF", + }, + templates: { + main: getTemplate(), + }, + }); + } + + /** + * + * @return {Monster.Components.Form.ToggleSwitch} + */ + [assembleMethodSymbol]() { + const self = this; + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + + /** + * init value to off + * if the value was not defined before inserting it into the HTML + */ + if (self.getOption("value") === null) { + self.setOption("value", self.getOption("values.off")); + } + + /** + * value from attribute + */ + if (self.hasAttribute("value")) { + self.setOption("value", self.getAttribute("value")); + } + + /** + * validate value + */ + validateAndSetValue.call(self); + + if (this.state === STATE_ON) { + toggleClassOn.call(self); + } else { + toggleClassOff.call(self); + } + + /** + * is called when options changed + */ + self[internalSymbol].attachObserver( + new Observer(function () { + if (isObject(this) && this instanceof ProxyObserver) { + validateAndSetValue.call(self); + toggleClass.call(self); + } + }), + ); + + return this; + } + + /** + * updater transformer methods for pipe + * + * @return {function} + */ + [updaterTransformerMethodsSymbol]() {; + return { + "state-callback": (Wert) => { + return this.state; + }, + }; + } + + /** + * @return [ToggleSwitchStyleSheet] + */ + static getCSSStyleSheet() { + return [ToggleSwitchStyleSheet]; + } - /** - * This method is called by the `instanceof` operator. - * @returns {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/toggle-switch@@instance"); - } - - static getTag() { - return "monster-toggle-switch"; - } - - /** - * To set the options via the html tag the attribute `data-monster-options` must be used. - * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} - * - * The individual configuration values can be found in the table. - * - * @property {string} value=current value of the element - * @property {Boolean} disabled=disabled=false Disabled state - * @property {Object} classes - * @property {string} classes.on=specifies the class for the on state. - * @property {string} classes.off=specifies the class for the off state. - * @property {Object} values - * @property {string} values.off=specifies the value of the element if it is not selected - * @property {Object} labels - * @property {string} labels.on=specifies the label for the on state. - * @property {string} labels.off=specifies the label for the off state. - * @property {Object} templates - * @property {string} templates.main=specifies the main template used by the control. - * - * @since 3.57.0 - */ - get defaults() { - return Object.assign({}, super.defaults, { - value: null, - disabled: false, - classes: { - "on": "monster-theme-primary-3", - "off": "monster-theme-primary-2" - }, - values: { - on: "on", - off: "off" - }, - labels: { - "toggle-switch-on": "ON", - "toggle-switch-off": "OFF", - }, - templates: { - main: getTemplate() - } - }) - } - - /** - * - * @return {Monster.Components.Form.Button} - */ - [assembleMethodSymbol]() { - const self = this; - super[assembleMethodSymbol](); - initControlReferences.call(this); - initEventHandler.call(this); - - /** - * init value to off - * if the value was not defined before inserting it into the HTML - */ - if (self.getOption("value") === null) { - self.setOption('value', self.getOption("values.off")); - } - - /** - * value from attribute - */ - if (self.hasAttribute("value")) { - self.setOption('value', self.getAttribute("value")); - } - - /** - * validate value - */ - validateAndSetValue.call(self); - - if(this.state === STATE_ON) { - toggleClassOn.call(self); - }else{ - toggleClassOff.call(self); - } - - /** - * is called when options changed - */ - self[internalSymbol].attachObserver( - new Observer(function () { - if (isObject(this) && this instanceof ProxyObserver) { - validateAndSetValue.call(self); - toggleClass.call(self); - } - }), - ); - - return this; - } - - /** - * updater transformer methods for pipe - * - * @return {function} - */ - [updaterTransformerMethodsSymbol]() { - const self = this; - return { - "state-callback": (Wert) => { - return self.state; - } - } - } - - /** - * @return [SwitchStyleSheet] - */ - static getCSSStyleSheet() { - return [ToggleSwitchStyleSheet]; - } - - /** - * toggle switch - * - * ``` + /** + * toggle switch + * + * ``` * e = document.querySelector('monster-toggle-switch'); * e.click() * ``` - */ - click() { - toggleValues.call(this); - } - - /** - * toggle switch on/off - * - * ``` + */ + click() { + toggleValues.call(this); + } + + /** + * toggle switch on/off + * + * ``` * e = document.querySelector('monster-toggle-switch'); * e.toggle() * ``` - * - * @return {ToggleSwitch} - */ - toggle() { - this.click(); - return this; - } - - /** - * toggle switch on - * - * ``` + * + * @return {ToggleSwitch} + */ + toggle() { + this.click(); + return this; + } + + /** + * toggle switch on + * + * ``` * e = document.querySelector('monster-toggle-switch'); * e.toggleOn() * ``` - * - * @return {ToggleSwitch} - */ - toggleOn() { - this.setOption('value', this.getOption('values.on')); - return this; - }; - - /** - * toggle switch off - * - * ``` + * + * @return {ToggleSwitch} + */ + toggleOn() { + this.setOption("value", this.getOption("values.on")); + return this; + } + + /** + * toggle switch off + * + * ``` * e = document.querySelector('monster-toggle-switch'); * e.toggleOff() * ``` - * - * @return {ToggleSwitch} - */ - toggleOff() { - this.setOption('value', this.getOption('values.off')); - return this; - }; - - /** - * returns the status of the element - * - * ``` + * + * @return {ToggleSwitch} + */ + toggleOff() { + this.setOption("value", this.getOption("values.off")); + return this; + } + + /** + * returns the status of the element + * + * ``` * e = document.querySelector('monster-toggle-switch'); * console.log(e.state) * // ↦ off * ``` - * - * @return {string} - */ - get state() { - return this.getOption('value') === this.getOption('values.on') ? STATE_ON : STATE_OFF; - } - - /** + * + * @return {string} + */ + get state() { + return this.getOption("value") === this.getOption("values.on") + ? STATE_ON + : STATE_OFF; + } + + /** * The current value of the Switch * * ``` @@ -286,11 +286,13 @@ class ToggleSwitch extends CustomControl { * * @return {string} */ - get value() { - return this.state === STATE_ON ? this.getOption('values.on') : this.getOption('values.off'); - } + get value() { + return this.state === STATE_ON + ? this.getOption("values.on") + : this.getOption("values.off"); + } - /** + /** * Set value * * ``` @@ -300,93 +302,94 @@ class ToggleSwitch extends CustomControl { * * @property {string} value */ - set value(value) { - this.setOption('value', value); - } - + set value(value) { + this.setOption("value", value); + } } /** * @private */ function initControlReferences() { - this[switchElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=switch]`, - ); + this[switchElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=switch]`, + ); } /** -* @private -*/ + * @private + */ function toggleClassOn() { - this[switchElementSymbol].classList.remove(this.getOption('classes.off')); // change color - this[switchElementSymbol].classList.add(this.getOption('classes.on'));// change color + this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color + this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color } /** -* @private -*/ + * @private + */ function toggleClassOff() { - this[switchElementSymbol].classList.remove(this.getOption('classes.on'));// change color - this[switchElementSymbol].classList.add(this.getOption('classes.off'));// change color + this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color + this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color } /** -* @private -*/ -function toggleClass() { - const self = this; - if (self.getOption('value') === self.getOption('values.on')) { - toggleClassOn.call(self); - } else { - toggleClassOff.call(self); - } + * @private + */ +function toggleClass() {; + if (this.getOption("value") === this.getOption("values.on")) { + toggleClassOn.call(this); + } else { + toggleClassOff.call(this); + } } /** * @private */ -function toggleValues() { - const self = this; - - if (self.getOption('disabled') === true) { - return; - } - - if (self.getOption('value') === self.getOption('values.on')) { - self.setOption('value', this.getOption('values.off')); - self?.setFormValue(self.getOption('value')); // set form value - } else { - self.setOption('value', this.getOption('values.on')); - self?.setFormValue(self.getOption('values.off')); // set form value - } - - self.setOption('state', self.state); +function toggleValues() {; + + if (this.getOption("disabled") === true) { + return; + } + + if (this.getOption("value") === this.getOption("values.on")) { + this.setOption("value", this.getOption("values.off")); + this?.setFormValue(this.getOption("value")); // set form value + } else { + this.setOption("value", this.getOption("values.on")); + this?.setFormValue(this.getOption("values.off")); // set form value + } + + this.setOption("state", this.state); } /** -* @private -*/ -function validateAndSetValue() { - let self = this; - let value = self.getOption('value'); - - let validatedValues = []; - validatedValues.push(this.getOption('values.on')); - validatedValues.push(this.getOption('values.off')); - - if (validatedValues.includes(value) === false) { - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - 'The value "' + value + '" must be "' + self.getOption("values.on") + '" or "' + self.getOption("values.off"), - ); - self.setOption('disabled', true); - self.formDisabledCallback(true); - } else { - self.setOption('disabled', false); - self.formDisabledCallback(false); - } + * @private + */ +function validateAndSetValue() {; + const value = this.getOption("value"); + + const validatedValues = []; + validatedValues.push(this.getOption("values.on")); + validatedValues.push(this.getOption("values.off")); + + if (validatedValues.includes(value) === false) { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + 'The value "' + + value + + '" must be "' + + this.getOption("values.on") + + '" or "' + + this.getOption("values.off"), + ); + this.setOption("disabled", true); + this.formDisabledCallback(true); + } else { + this.setOption("disabled", false); + this.formDisabledCallback(false); + } } /** @@ -394,16 +397,16 @@ function validateAndSetValue() { * @return {initEventHandler} */ function initEventHandler() { - const self = this; - self.addEventListener("keyup", function (event) { - if (event.code === 'Space') { - self[switchElementSymbol].click(); - } - }); - self.addEventListener("click", function (event) { - toggleValues.call(self); - }); - return this; + const self = this; + self.addEventListener("keyup", function (event) { + if (event.code === "Space") { + self[switchElementSymbol].click(); + } + }); + self.addEventListener("click", function (event) { + toggleValues.call(self); + }); + return this; } /** @@ -411,8 +414,8 @@ function initEventHandler() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div data-monster-role="control" part="control" tabindex="0"> <div class="switch" data-monster-role="switch" data-monster-attributes="data-monster-state path:value | call:state-callback " > @@ -424,4 +427,4 @@ function getTemplate() { </div>`; } -registerCustomElement(ToggleSwitch); \ No newline at end of file +registerCustomElement(ToggleSwitch); diff --git a/source/data/transformer.mjs b/source/data/transformer.mjs index 9580e33a1d488afebb0eec80235a84d36b92e08a..f8bf9abaa1d31472b280b17f2e9b05c216103428 100644 --- a/source/data/transformer.mjs +++ b/source/data/transformer.mjs @@ -775,16 +775,16 @@ function transform(value) { defaultValue = convertSpecialStrings(defaultValue, value); return translations.getText(key, defaultValue); - + case "set-toggle": case "set-set": case "set-remove": - let modifier = args.shift(); + const modifier = args.shift(); let delimiter = args.shift(); if (delimiter === undefined) { delimiter = " "; } - + const set = new Set(value.split(delimiter)); const toggle = new Set(modifier.split(delimiter)); if (this.command === "set-toggle") { @@ -805,9 +805,7 @@ function transform(value) { } } return Array.from(set).join(delimiter); - - - + default: throw new Error(`unknown command ${this.command}`); } diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs index 7a04c4761abe668729f8820650484fb511925cce..ff72524c9bec7b68d2cf0c83e5614a9a0a5d387a 100644 --- a/source/dom/customelement.mjs +++ b/source/dom/customelement.mjs @@ -5,62 +5,60 @@ * 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, + validateFunction, + validateInstance, + validateObject, } 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 { - 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"; +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 + CustomElement, + initMethodSymbol, + assembleMethodSymbol, + attributeObserverSymbol, + registerCustomElement, + getSlottedElements, + updaterTransformerMethodsSymbol, }; /** @@ -74,7 +72,7 @@ const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol"); * @type {symbol} */ const assembleMethodSymbol = Symbol.for( - "@schukai/monster/dom/@@assembleMethodSymbol", + "@schukai/monster/dom/@@assembleMethodSymbol", ); /** @@ -82,7 +80,7 @@ const assembleMethodSymbol = Symbol.for( * @type {symbol} */ const updaterTransformerMethodsSymbol = Symbol.for( - "@schukai/monster/dom/@@updaterTransformerMethodsSymbol", + "@schukai/monster/dom/@@updaterTransformerMethodsSymbol", ); /** @@ -91,7 +89,7 @@ const updaterTransformerMethodsSymbol = Symbol.for( * @type {symbol} */ const attributeObserverSymbol = Symbol.for( - "@schukai/monster/dom/@@attributeObserver", + "@schukai/monster/dom/@@attributeObserver", ); /** @@ -99,7 +97,7 @@ const attributeObserverSymbol = Symbol.for( * @type {symbol} */ const attributeMutationObserverSymbol = Symbol( - "@schukai/monster/dom/@@mutationObserver", + "@schukai/monster/dom/@@mutationObserver", ); /** @@ -238,517 +236,513 @@ 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 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); - } + /** + * 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); + } } /** @@ -757,50 +751,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`, + ); } /** @@ -817,16 +811,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); } /** @@ -836,35 +830,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()); + } } /** @@ -874,19 +868,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; } /** @@ -896,89 +890,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); + } + }; } /** @@ -987,37 +981,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; } /** @@ -1025,21 +1019,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 {}; } /** @@ -1051,26 +1045,25 @@ 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); } /** @@ -1078,21 +1071,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; } /** @@ -1105,49 +1098,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; } /** @@ -1160,35 +1153,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; } /** @@ -1203,20 +1196,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); } diff --git a/source/dom/updater.mjs b/source/dom/updater.mjs index 7b80b813130d150360de5cfde780170017492f1f..4de2a8225a375b34053acb5f14034f3537329d47 100644 --- a/source/dom/updater.mjs +++ b/source/dom/updater.mjs @@ -5,36 +5,36 @@ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ -import {internalSymbol} from "../constants.mjs"; -import {diff} from "../data/diff.mjs"; -import {Pathfinder} from "../data/pathfinder.mjs"; -import {Pipe} from "../data/pipe.mjs"; +import { internalSymbol } from "../constants.mjs"; +import { diff } from "../data/diff.mjs"; +import { Pathfinder } from "../data/pathfinder.mjs"; +import { Pipe } from "../data/pipe.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_UPDATER_ATTRIBUTES, - ATTRIBUTE_UPDATER_BIND, - ATTRIBUTE_UPDATER_BIND_TYPE, - ATTRIBUTE_UPDATER_INSERT, - ATTRIBUTE_UPDATER_INSERT_REFERENCE, - ATTRIBUTE_UPDATER_REMOVE, - ATTRIBUTE_UPDATER_REPLACE, - ATTRIBUTE_UPDATER_SELECT_THIS, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_UPDATER_ATTRIBUTES, + ATTRIBUTE_UPDATER_BIND, + ATTRIBUTE_UPDATER_BIND_TYPE, + ATTRIBUTE_UPDATER_INSERT, + ATTRIBUTE_UPDATER_INSERT_REFERENCE, + ATTRIBUTE_UPDATER_REMOVE, + ATTRIBUTE_UPDATER_REPLACE, + ATTRIBUTE_UPDATER_SELECT_THIS, } from "./constants.mjs"; -import {Base} from "../types/base.mjs"; -import {isArray, isString, isInstance, isIterable} from "../types/is.mjs"; -import {Observer} from "../types/observer.mjs"; -import {ProxyObserver} from "../types/proxyobserver.mjs"; -import {validateArray, validateInstance} from "../types/validate.mjs"; -import {Sleep} from "../util/sleep.mjs"; -import {clone} from "../util/clone.mjs"; -import {trimSpaces} from "../util/trimspaces.mjs"; -import {addAttributeToken, addToObjectLink} from "./attributes.mjs"; -import {updaterTransformerMethodsSymbol} from "./customelement.mjs"; -import {findTargetElementFromEvent} from "./events.mjs"; -import {findDocumentTemplate} from "./template.mjs"; - -export {Updater, addObjectWithUpdaterToElement}; +import { Base } from "../types/base.mjs"; +import { isArray, isString, isInstance, isIterable } from "../types/is.mjs"; +import { Observer } from "../types/observer.mjs"; +import { ProxyObserver } from "../types/proxyobserver.mjs"; +import { validateArray, validateInstance } from "../types/validate.mjs"; +import { Sleep } from "../util/sleep.mjs"; +import { clone } from "../util/clone.mjs"; +import { trimSpaces } from "../util/trimspaces.mjs"; +import { addAttributeToken, addToObjectLink } from "./attributes.mjs"; +import { updaterTransformerMethodsSymbol } from "./customelement.mjs"; +import { findTargetElementFromEvent } from "./events.mjs"; +import { findDocumentTemplate } from "./template.mjs"; + +export { Updater, addObjectWithUpdaterToElement }; /** * The updater class connects an object with the dom. In this way, structures and contents in the DOM can be @@ -61,182 +61,182 @@ export {Updater, addObjectWithUpdaterToElement}; * @summary The updater class connects an object with the dom */ class Updater extends Base { - /** - * @since 1.8.0 - * @param {HTMLElement} element - * @param {object|ProxyObserver|undefined} subject - * @throws {TypeError} value is not a object - * @throws {TypeError} value is not an instance of HTMLElement - * @see {@link Monster.DOM.findDocumentTemplate} - */ - constructor(element, subject) { - super(); - - /** - * @type {HTMLElement} - */ - if (subject === undefined) subject = {}; - if (!isInstance(subject, ProxyObserver)) { - subject = new ProxyObserver(subject); - } - - this[internalSymbol] = { - element: validateInstance(element, HTMLElement), - last: {}, - callbacks: new Map(), - eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"], - subject: subject, - }; - - this[internalSymbol].callbacks.set( - "checkstate", - getCheckStateCallback.call(this), - ); - - this[internalSymbol].subject.attachObserver( - new Observer(() => { - const s = this[internalSymbol].subject.getRealSubject(); - - const diffResult = diff(this[internalSymbol].last, s); - this[internalSymbol].last = clone(s); - - const promises = []; - - for (const [, change] of Object.entries(diffResult)) { - promises.push( - Sleep(1).then(() => { - removeElement.call(this, change); - insertElement.call(this, change); - updateContent.call(this, change); - updateAttributes.call(this, change); - }), - ); - } - - return Promise.all(promises); - }), - ); - } - - /** - * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend' - * - * @see {@link https://developer.mozilla.org/de/docs/Web/Events} - * @since 1.9.0 - * @param {Array} types - * @return {Updater} - */ - setEventTypes(types) { - this[internalSymbol].eventTypes = validateArray(types); - return this; - } - - /** - * With this method, the eventlisteners are hooked in and the magic begins. - * - * ``` - * updater.run().then(() => { - * updater.enableEventProcessing(); - * }); - * ``` - * - * @since 1.9.0 - * @return {Updater} - * @throws {Error} the bind argument must start as a value with a path - */ - enableEventProcessing() { - this.disableEventProcessing(); - - for (const type of this[internalSymbol].eventTypes) { - // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - this[internalSymbol].element.addEventListener( - type, - getControlEventHandler.call(this), - { - capture: true, - passive: true, - }, - ); - } - - return this; - } - - /** - * This method turns off the magic or who loves it more profane it removes the eventListener. - * - * @since 1.9.0 - * @return {Updater} - */ - disableEventProcessing() { - for (const type of this[internalSymbol].eventTypes) { - this[internalSymbol].element.removeEventListener( - type, - getControlEventHandler.call(this), - ); - } - - return this; - } - - /** - * The run method must be called for the update to start working. - * The method ensures that changes are detected. - * - * ``` - * updater.run().then(() => { - * updater.enableEventProcessing(); - * }); - * ``` - * - * @summary Let the magic begin - * @return {Promise} - */ - run() { - // the key __init__has no further meaning and is only - // used to create the diff for empty objects. - this[internalSymbol].last = {__init__: true}; - return this[internalSymbol].subject.notifyObservers(); - } - - /** - * Gets the values of bound elements and changes them in subject - * - * @since 1.27.0 - * @return {Monster.DOM.Updater} - */ - retrieve() { - retrieveFromBindings.call(this); - return this; - } - - /** - * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here. - * However, if you passed a simple object, here you will get a proxy for that object. - * - * For changes the ProxyObserver must be used. - * - * @since 1.8.0 - * @return {Proxy} - */ - getSubject() { - return this[internalSymbol].subject.getSubject(); - } - - /** - * This method can be used to register commands that can be called via call: instruction. - * This can be used to provide a pipe with its own functionality. - * - * @param {string} name - * @param {function} callback - * @returns {Transformer} - * @throws {TypeError} value is not a string - * @throws {TypeError} value is not a function - */ - setCallback(name, callback) { - this[internalSymbol].callbacks.set(name, callback); - return this; - } + /** + * @since 1.8.0 + * @param {HTMLElement} element + * @param {object|ProxyObserver|undefined} subject + * @throws {TypeError} value is not a object + * @throws {TypeError} value is not an instance of HTMLElement + * @see {@link Monster.DOM.findDocumentTemplate} + */ + constructor(element, subject) { + super(); + + /** + * @type {HTMLElement} + */ + if (subject === undefined) subject = {}; + if (!isInstance(subject, ProxyObserver)) { + subject = new ProxyObserver(subject); + } + + this[internalSymbol] = { + element: validateInstance(element, HTMLElement), + last: {}, + callbacks: new Map(), + eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"], + subject: subject, + }; + + this[internalSymbol].callbacks.set( + "checkstate", + getCheckStateCallback.call(this), + ); + + this[internalSymbol].subject.attachObserver( + new Observer(() => { + const s = this[internalSymbol].subject.getRealSubject(); + + const diffResult = diff(this[internalSymbol].last, s); + this[internalSymbol].last = clone(s); + + const promises = []; + + for (const [, change] of Object.entries(diffResult)) { + promises.push( + Sleep(1).then(() => { + removeElement.call(this, change); + insertElement.call(this, change); + updateContent.call(this, change); + updateAttributes.call(this, change); + }), + ); + } + + return Promise.all(promises); + }), + ); + } + + /** + * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend' + * + * @see {@link https://developer.mozilla.org/de/docs/Web/Events} + * @since 1.9.0 + * @param {Array} types + * @return {Updater} + */ + setEventTypes(types) { + this[internalSymbol].eventTypes = validateArray(types); + return this; + } + + /** + * With this method, the eventlisteners are hooked in and the magic begins. + * + * ``` + * updater.run().then(() => { + * updater.enableEventProcessing(); + * }); + * ``` + * + * @since 1.9.0 + * @return {Updater} + * @throws {Error} the bind argument must start as a value with a path + */ + enableEventProcessing() { + this.disableEventProcessing(); + + for (const type of this[internalSymbol].eventTypes) { + // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + this[internalSymbol].element.addEventListener( + type, + getControlEventHandler.call(this), + { + capture: true, + passive: true, + }, + ); + } + + return this; + } + + /** + * This method turns off the magic or who loves it more profane it removes the eventListener. + * + * @since 1.9.0 + * @return {Updater} + */ + disableEventProcessing() { + for (const type of this[internalSymbol].eventTypes) { + this[internalSymbol].element.removeEventListener( + type, + getControlEventHandler.call(this), + ); + } + + return this; + } + + /** + * The run method must be called for the update to start working. + * The method ensures that changes are detected. + * + * ``` + * updater.run().then(() => { + * updater.enableEventProcessing(); + * }); + * ``` + * + * @summary Let the magic begin + * @return {Promise} + */ + run() { + // the key __init__has no further meaning and is only + // used to create the diff for empty objects. + this[internalSymbol].last = { __init__: true }; + return this[internalSymbol].subject.notifyObservers(); + } + + /** + * Gets the values of bound elements and changes them in subject + * + * @since 1.27.0 + * @return {Monster.DOM.Updater} + */ + retrieve() { + retrieveFromBindings.call(this); + return this; + } + + /** + * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here. + * However, if you passed a simple object, here you will get a proxy for that object. + * + * For changes the ProxyObserver must be used. + * + * @since 1.8.0 + * @return {Proxy} + */ + getSubject() { + return this[internalSymbol].subject.getSubject(); + } + + /** + * This method can be used to register commands that can be called via call: instruction. + * This can be used to provide a pipe with its own functionality. + * + * @param {string} name + * @param {function} callback + * @returns {Transformer} + * @throws {TypeError} value is not a string + * @throws {TypeError} value is not a function + */ + setCallback(name, callback) { + this[internalSymbol].callbacks.set(name, callback); + return this; + } } /** @@ -247,20 +247,20 @@ class Updater extends Base { * @this Updater */ function getCheckStateCallback() { - return function (current) { - // this is a reference to the current object (therefore no array function here) - if (this instanceof HTMLInputElement) { - if (["radio", "checkbox"].indexOf(this.type) !== -1) { - return `${this.value}` === `${current}` ? "true" : undefined; - } - } else if (this instanceof HTMLOptionElement) { - if (isArray(current) && current.indexOf(this.value) !== -1) { - return "true"; - } - - return undefined; - } - }; + return function (current) { + // this is a reference to the current object (therefore no array function here) + if (this instanceof HTMLInputElement) { + if (["radio", "checkbox"].indexOf(this.type) !== -1) { + return `${this.value}` === `${current}` ? "true" : undefined; + } + } else if (this instanceof HTMLOptionElement) { + if (isArray(current) && current.indexOf(this.value) !== -1) { + return "true"; + } + + return undefined; + } + }; } /** @@ -275,27 +275,27 @@ const symbol = Symbol("@schukai/monster/updater@@EventHandler"); * @throws {Error} the bind argument must start as a value with a path */ function getControlEventHandler() { - if (this[symbol]) { - return this[symbol]; - } - - /** - * @throws {Error} the bind argument must start as a value with a path. - * @throws {Error} unsupported object - * @param {Event} event - */ - this[symbol] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND); - - if (element === undefined) { - return; - } -setTimeout(() => { - retrieveAndSetValue.call(this, element) -}, 0); - }; - - return this[symbol]; + if (this[symbol]) { + return this[symbol]; + } + + /** + * @throws {Error} the bind argument must start as a value with a path. + * @throws {Error} unsupported object + * @param {Event} event + */ + this[symbol] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND); + + if (element === undefined) { + return; + } + setTimeout(() => { + retrieveAndSetValue.call(this, element); + }, 0); + }; + + return this[symbol]; } /** @@ -306,101 +306,100 @@ setTimeout(() => { * @private */ function retrieveAndSetValue(element) { - - const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject()); - - let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND); - if (path === null) - throw new Error("the bind argument must start as a value with a path"); - - if (path.indexOf("path:") !== 0) { - throw new Error("the bind argument must start as a value with a path"); - } - - path = path.substring(5); // remove path: from the string - - let value; - - if (element instanceof HTMLInputElement) { - switch (element.type) { - case "checkbox": - value = element.checked ? element.value : undefined; - break; - default: - value = element.value; - break; - } - } else if (element instanceof HTMLTextAreaElement) { - value = element.value; - } else if (element instanceof HTMLSelectElement) { - switch (element.type) { - case "select-one": - value = element.value; - break; - case "select-multiple": - value = element.value; - - let options = element?.selectedOptions; - if (options === undefined) - options = element.querySelectorAll(":scope option:checked"); - value = Array.from(options).map(({value}) => value); - - break; - } - - // values from custom elements - } else if ( - (element?.constructor?.prototype && - !!Object.getOwnPropertyDescriptor( - element.constructor.prototype, - "value", - )?.["get"]) || - element.hasOwnProperty("value") - ) { - value = element?.["value"]; - } else { - throw new Error("unsupported object"); - } - - if (isString(value)) { - const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE); - switch (type) { - case "number": - case "int": - case "float": - case "integer": - value = Number(value); - if (isNaN(value)) { - value = 0; - } - break; - case "boolean": - case "bool": - case "checkbox": - value = value === "true" || value === "1" || value === "on"; - break; - case "array": - case "list": - value = value.split(","); - break; - case "object": - case "json": - value = JSON.parse(value); - break; - default: - break; - } - } - - const copy = clone(this[internalSymbol].subject.getRealSubject()); - const pf = new Pathfinder(copy); - pf.setVia(path, value); - - const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject()); - - if (diffResult.length > 0) { - pathfinder.setVia(path, value); - } + const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject()); + + let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND); + if (path === null) + throw new Error("the bind argument must start as a value with a path"); + + if (path.indexOf("path:") !== 0) { + throw new Error("the bind argument must start as a value with a path"); + } + + path = path.substring(5); // remove path: from the string + + let value; + + if (element instanceof HTMLInputElement) { + switch (element.type) { + case "checkbox": + value = element.checked ? element.value : undefined; + break; + default: + value = element.value; + break; + } + } else if (element instanceof HTMLTextAreaElement) { + value = element.value; + } else if (element instanceof HTMLSelectElement) { + switch (element.type) { + case "select-one": + value = element.value; + break; + case "select-multiple": + value = element.value; + + let options = element?.selectedOptions; + if (options === undefined) + options = element.querySelectorAll(":scope option:checked"); + value = Array.from(options).map(({ value }) => value); + + break; + } + + // values from custom elements + } else if ( + (element?.constructor?.prototype && + !!Object.getOwnPropertyDescriptor( + element.constructor.prototype, + "value", + )?.["get"]) || + element.hasOwnProperty("value") + ) { + value = element?.["value"]; + } else { + throw new Error("unsupported object"); + } + + if (isString(value)) { + const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE); + switch (type) { + case "number": + case "int": + case "float": + case "integer": + value = Number(value); + if (isNaN(value)) { + value = 0; + } + break; + case "boolean": + case "bool": + case "checkbox": + value = value === "true" || value === "1" || value === "on"; + break; + case "array": + case "list": + value = value.split(","); + break; + case "object": + case "json": + value = JSON.parse(value); + break; + default: + break; + } + } + + const copy = clone(this[internalSymbol].subject.getRealSubject()); + const pf = new Pathfinder(copy); + pf.setVia(path, value); + + const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject()); + + if (diffResult.length > 0) { + pathfinder.setVia(path, value); + } } /** @@ -410,15 +409,15 @@ function retrieveAndSetValue(element) { * @private */ function retrieveFromBindings() { - if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) { - retrieveAndSetValue.call(this, this[internalSymbol].element); - } - - for (const [, element] of this[internalSymbol].element - .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`) - .entries()) { - retrieveAndSetValue.call(this, element); - } + if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) { + retrieveAndSetValue.call(this, this[internalSymbol].element); + } + + for (const [, element] of this[internalSymbol].element + .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`) + .entries()) { + retrieveAndSetValue.call(this, element); + } } /** @@ -429,11 +428,11 @@ function retrieveFromBindings() { * @return {void} */ function removeElement(change) { - for (const [, element] of this[internalSymbol].element - .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`) - .entries()) { - element.parentNode.removeChild(element); - } + for (const [, element] of this[internalSymbol].element + .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`) + .entries()) { + element.parentNode.removeChild(element); + } } /** @@ -449,133 +448,133 @@ function removeElement(change) { * @this Updater */ function insertElement(change) { - const subject = this[internalSymbol].subject.getRealSubject(); + const subject = this[internalSymbol].subject.getRealSubject(); - const mem = new WeakSet(); - let wd = 0; + const mem = new WeakSet(); + let wd = 0; - const container = this[internalSymbol].element; + const container = this[internalSymbol].element; - while (true) { - let found = false; - wd++; - - const p = clone(change?.["path"]); - if (!isArray(p)) return; - - while (p.length > 0) { - const current = p.join("."); - - let iterator = new Set(); - const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`; - - const e = container.querySelectorAll(query); - - if (e.length > 0) { - iterator = new Set([...e]); - } - - if (container.matches(query)) { - iterator.add(container); - } - - for (const [, containerElement] of iterator.entries()) { - if (mem.has(containerElement)) continue; - mem.add(containerElement); - - found = true; - - const attributes = containerElement.getAttribute( - ATTRIBUTE_UPDATER_INSERT, - ); - if (attributes === null) continue; - - const def = trimSpaces(attributes); - const i = def.indexOf(" "); - const key = trimSpaces(def.substr(0, i)); - const refPrefix = `${key}-`; - const cmd = trimSpaces(def.substr(i)); - - // this case is actually excluded by the query but is nevertheless checked again here - if (cmd.indexOf("|") > 0) { - throw new Error("pipes are not allowed when cloning a node."); - } - - const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f); - }); - - let value; - try { - containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } - - const dataPath = cmd.split(":").pop(); - - let insertPoint; - if (containerElement.hasChildNodes()) { - insertPoint = containerElement.lastChild; - } - - if (!isIterable(value)) { - throw new Error("the value is not iterable"); - } - - const available = new Set(); - - for (const [i, obj] of Object.entries(value)) { - const ref = refPrefix + i; - const currentPath = `${dataPath}.${i}`; - - available.add(ref); - const refElement = containerElement.querySelector( - `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`, - ); - - if (refElement instanceof HTMLElement) { - insertPoint = refElement; - continue; - } - - appendNewDocumentFragment(containerElement, key, ref, currentPath); - } - - const nodes = containerElement.querySelectorAll( - `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`, - ); - - for (const [, node] of Object.entries(nodes)) { - if ( - !available.has( - node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE), - ) - ) { - try { - containerElement.removeChild(node); - } catch (e) { - containerElement.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ - e.message - }`.trim(), - ); - } - } - } - } - - p.pop(); - } - - if (found === false) break; - if (wd++ > 200) { - throw new Error("the maximum depth for the recursion is reached."); - } - } + while (true) { + let found = false; + wd++; + + const p = clone(change?.["path"]); + if (!isArray(p)) return; + + while (p.length > 0) { + const current = p.join("."); + + let iterator = new Set(); + const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`; + + const e = container.querySelectorAll(query); + + if (e.length > 0) { + iterator = new Set([...e]); + } + + if (container.matches(query)) { + iterator.add(container); + } + + for (const [, containerElement] of iterator.entries()) { + if (mem.has(containerElement)) continue; + mem.add(containerElement); + + found = true; + + const attributes = containerElement.getAttribute( + ATTRIBUTE_UPDATER_INSERT, + ); + if (attributes === null) continue; + + const def = trimSpaces(attributes); + const i = def.indexOf(" "); + const key = trimSpaces(def.substr(0, i)); + const refPrefix = `${key}-`; + const cmd = trimSpaces(def.substr(i)); + + // this case is actually excluded by the query but is nevertheless checked again here + if (cmd.indexOf("|") > 0) { + throw new Error("pipes are not allowed when cloning a node."); + } + + const pipe = new Pipe(cmd); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f); + }); + + let value; + try { + containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } + + const dataPath = cmd.split(":").pop(); + + let insertPoint; + if (containerElement.hasChildNodes()) { + insertPoint = containerElement.lastChild; + } + + if (!isIterable(value)) { + throw new Error("the value is not iterable"); + } + + const available = new Set(); + + for (const [i, obj] of Object.entries(value)) { + const ref = refPrefix + i; + const currentPath = `${dataPath}.${i}`; + + available.add(ref); + const refElement = containerElement.querySelector( + `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`, + ); + + if (refElement instanceof HTMLElement) { + insertPoint = refElement; + continue; + } + + appendNewDocumentFragment(containerElement, key, ref, currentPath); + } + + const nodes = containerElement.querySelectorAll( + `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`, + ); + + for (const [, node] of Object.entries(nodes)) { + if ( + !available.has( + node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE), + ) + ) { + try { + containerElement.removeChild(node); + } catch (e) { + containerElement.setAttribute( + ATTRIBUTE_ERRORMESSAGE, + `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ + e.message + }`.trim(), + ); + } + } + } + } + + p.pop(); + } + + if (found === false) break; + if (wd++ > 200) { + throw new Error("the maximum depth for the recursion is reached."); + } + } } /** @@ -590,17 +589,17 @@ function insertElement(change) { * @throws {Error} no template was found with the specified key. */ function appendNewDocumentFragment(container, key, ref, path) { - const template = findDocumentTemplate(key, container); + const template = findDocumentTemplate(key, container); - const nodes = template.createDocumentFragment(); - for (const [, node] of Object.entries(nodes.childNodes)) { - if (node instanceof HTMLElement) { - applyRecursive(node, key, path); - node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref); - } + const nodes = template.createDocumentFragment(); + for (const [, node] of Object.entries(nodes.childNodes)) { + if (node instanceof HTMLElement) { + applyRecursive(node, key, path); + node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref); + } - container.appendChild(node); - } + container.appendChild(node); + } } /** @@ -613,27 +612,27 @@ function appendNewDocumentFragment(container, key, ref, path) { * @return {void} */ function applyRecursive(node, key, path) { - if (node instanceof HTMLElement) { - if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) { - const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE); - node.setAttribute( - ATTRIBUTE_UPDATER_REPLACE, - value.replaceAll(`path:${key}`, `path:${path}`), - ); - } - - if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { - const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); - node.setAttribute( - ATTRIBUTE_UPDATER_ATTRIBUTES, - value.replaceAll(`path:${key}`, `path:${path}`), - ); - } - - for (const [, child] of Object.entries(node.childNodes)) { - applyRecursive(child, key, path); - } - } + if (node instanceof HTMLElement) { + if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) { + const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE); + node.setAttribute( + ATTRIBUTE_UPDATER_REPLACE, + value.replaceAll(`path:${key}`, `path:${path}`), + ); + } + + if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { + const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); + node.setAttribute( + ATTRIBUTE_UPDATER_ATTRIBUTES, + value.replaceAll(`path:${key}`, `path:${path}`), + ); + } + + for (const [, child] of Object.entries(node.childNodes)) { + applyRecursive(child, key, path); + } + } } /** @@ -645,19 +644,19 @@ function applyRecursive(node, key, path) { * @this Updater */ function updateContent(change) { - const subject = this[internalSymbol].subject.getRealSubject(); - - const p = clone(change?.["path"]); - runUpdateContent.call(this, this[internalSymbol].element, p, subject); - - const slots = this[internalSymbol].element.querySelectorAll("slot"); - if (slots.length > 0) { - for (const [, slot] of Object.entries(slots)) { - for (const [, element] of Object.entries(slot.assignedNodes())) { - runUpdateContent.call(this, element, p, subject); - } - } - } + const subject = this[internalSymbol].subject.getRealSubject(); + + const p = clone(change?.["path"]); + runUpdateContent.call(this, this[internalSymbol].element, p, subject); + + const slots = this[internalSymbol].element.querySelectorAll("slot"); + if (slots.length > 0) { + for (const [, slot] of Object.entries(slots)) { + for (const [, element] of Object.entries(slot.assignedNodes())) { + runUpdateContent.call(this, element, p, subject); + } + } + } } /** @@ -670,69 +669,69 @@ function updateContent(change) { * @return {void} */ function runUpdateContent(container, parts, subject) { - if (!isArray(parts)) return; - if (!(container instanceof HTMLElement)) return; - parts = clone(parts); - - const mem = new WeakSet(); - - while (parts.length > 0) { - const current = parts.join("."); - parts.pop(); - - // Unfortunately, static data is always changed as well, since it is not possible to react to changes here. - const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`; - const e = container.querySelectorAll(`${query}`); - - const iterator = new Set([...e]); - - if (container.matches(query)) { - iterator.add(container); - } - - /** - * @type {HTMLElement} - */ - for (const [element] of iterator.entries()) { - if (mem.has(element)) return; - mem.add(element); - - const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE); - const cmd = trimSpaces(attributes); - - const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f); - }); - - let value; - try { - element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } - - if (value instanceof HTMLElement) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - try { - element.appendChild(value); - } catch (e) { - element.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ - e.message - }`.trim(), - ); - } - } else { - element.innerHTML = value; - } - } - } + if (!isArray(parts)) return; + if (!(container instanceof HTMLElement)) return; + parts = clone(parts); + + const mem = new WeakSet(); + + while (parts.length > 0) { + const current = parts.join("."); + parts.pop(); + + // Unfortunately, static data is always changed as well, since it is not possible to react to changes here. + const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`; + const e = container.querySelectorAll(`${query}`); + + const iterator = new Set([...e]); + + if (container.matches(query)) { + iterator.add(container); + } + + /** + * @type {HTMLElement} + */ + for (const [element] of iterator.entries()) { + if (mem.has(element)) return; + mem.add(element); + + const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE); + const cmd = trimSpaces(attributes); + + const pipe = new Pipe(cmd); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f); + }); + + let value; + try { + element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } + + if (value instanceof HTMLElement) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + try { + element.appendChild(value); + } catch (e) { + element.setAttribute( + ATTRIBUTE_ERRORMESSAGE, + `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ + e.message + }`.trim(), + ); + } + } else { + element.innerHTML = value; + } + } + } } /** @@ -744,9 +743,9 @@ function runUpdateContent(container, parts, subject) { * @return {void} */ function updateAttributes(change) { - const subject = this[internalSymbol].subject.getRealSubject(); - const p = clone(change?.["path"]); - runUpdateAttributes.call(this, this[internalSymbol].element, p, subject); + const subject = this[internalSymbol].subject.getRealSubject(); + const p = clone(change?.["path"]); + runUpdateAttributes.call(this, this[internalSymbol].element, p, subject); } /** @@ -758,70 +757,70 @@ function updateAttributes(change) { * @this Updater */ function runUpdateAttributes(container, parts, subject) { - if (!isArray(parts)) return; - parts = clone(parts); + if (!isArray(parts)) return; + parts = clone(parts); - const mem = new WeakSet(); + const mem = new WeakSet(); - while (parts.length > 0) { - const current = parts.join("."); - parts.pop(); + while (parts.length > 0) { + const current = parts.join("."); + parts.pop(); - let iterator = new Set(); + let iterator = new Set(); - const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`; + const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`; - const e = container.querySelectorAll(query); + const e = container.querySelectorAll(query); - if (e.length > 0) { - iterator = new Set([...e]); - } + if (e.length > 0) { + iterator = new Set([...e]); + } - if (container.matches(query)) { - iterator.add(container); - } + if (container.matches(query)) { + iterator.add(container); + } - for (const [element] of iterator.entries()) { - if (mem.has(element)) return; - mem.add(element); + for (const [element] of iterator.entries()) { + if (mem.has(element)) return; + mem.add(element); - // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set - if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { - continue; - } + // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set + if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { + continue; + } - const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); + const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); - for (let [, def] of Object.entries(attributes.split(","))) { - def = trimSpaces(def); - const i = def.indexOf(" "); - const name = trimSpaces(def.substr(0, i)); - const cmd = trimSpaces(def.substr(i)); + for (let [, def] of Object.entries(attributes.split(","))) { + def = trimSpaces(def); + const i = def.indexOf(" "); + const name = trimSpaces(def.substr(0, i)); + const cmd = trimSpaces(def.substr(i)); - const pipe = new Pipe(cmd); + const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f, element); - }); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f, element); + }); - let value; - try { - element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } + let value; + try { + element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } - if (value === undefined) { - element.removeAttribute(name); - } else if (element.getAttribute(name) !== value) { - element.setAttribute(name, value); - } + if (value === undefined) { + element.removeAttribute(name); + } else if (element.getAttribute(name) !== value) { + element.setAttribute(name, value); + } - handleInputControlAttributeUpdate.call(this, element, name, value); - } - } - } + handleInputControlAttributeUpdate.call(this, element, name, value); + } + } + } } /** @@ -834,58 +833,58 @@ function runUpdateAttributes(container, parts, subject) { */ function handleInputControlAttributeUpdate(element, name, value) { - if (element instanceof HTMLSelectElement) { - switch (element.type) { - case "select-multiple": - for (const [index, opt] of Object.entries(element.options)) { - if (value.indexOf(opt.value) !== -1) { - opt.selected = true; - } else { - opt.selected = false; - } - } - - break; - case "select-one": - // Only one value may be selected - - for (const [index, opt] of Object.entries(element.options)) { - if (opt.value === value) { - element.selectedIndex = index; - break; - } - } - - break; - } - } else if (element instanceof HTMLInputElement) { - switch (element.type) { - case "radio": - if (name === "checked") { - element.checked = value !== undefined; - } - - break; - - case "checkbox": - if (name === "checked") { - element.checked = value !== undefined; - } - - break; - case "text": - default: - if (name === "value") { - element.value = value === undefined ? "" : value; - } - - break; - } - } else if (element instanceof HTMLTextAreaElement) { - if (name === "value") { - element.value = value === undefined ? "" : value; - } - } + if (element instanceof HTMLSelectElement) { + switch (element.type) { + case "select-multiple": + for (const [index, opt] of Object.entries(element.options)) { + if (value.indexOf(opt.value) !== -1) { + opt.selected = true; + } else { + opt.selected = false; + } + } + + break; + case "select-one": + // Only one value may be selected + + for (const [index, opt] of Object.entries(element.options)) { + if (opt.value === value) { + element.selectedIndex = index; + break; + } + } + + break; + } + } else if (element instanceof HTMLInputElement) { + switch (element.type) { + case "radio": + if (name === "checked") { + element.checked = value !== undefined; + } + + break; + + case "checkbox": + if (name === "checked") { + element.checked = value !== undefined; + } + + break; + case "text": + default: + if (name === "value") { + element.value = value === undefined ? "" : value; + } + + break; + } + } else if (element instanceof HTMLTextAreaElement) { + if (name === "value") { + element.value = value === undefined ? "" : value; + } + } } /** @@ -901,73 +900,79 @@ function handleInputControlAttributeUpdate(element, name, value) { * @throws {TypeError} symbol must be an instance of Symbol */ function addObjectWithUpdaterToElement(elements, symbol, object) { - if (!(this instanceof HTMLElement)) { - throw new TypeError( - "the context of this function must be an instance of HTMLElement", - ); - } - - if (!(typeof symbol === "symbol")) { - throw new TypeError("symbol must be an instance of Symbol"); - } - - const updaters = new Set(); - - if (elements instanceof NodeList) { - elements = new Set([...elements]); - } else if (elements instanceof HTMLElement) { - elements = new Set([elements]); - } else if (elements instanceof Set) { - } else { - throw new TypeError( - `elements is not a valid type. (actual: ${typeof elements})`, - ); - } - - const result = []; - - let updaterCallbacks = [] - const cb = this?.[updaterTransformerMethodsSymbol] - if (this instanceof HTMLElement && (typeof cb === "function")) { - - let callbacks = cb.call(this); - if (typeof callbacks === "object") { - for (const [name, callback] of Object.entries(callbacks)) { - if (typeof callback === "function") { - updaterCallbacks.push([name, callback]); - } else { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `onUpdaterPipeCallbacks: ${name} is not a function`); - } - } - } else { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `onUpdaterPipeCallbacks do not return an object with functions`); - } - } - - elements.forEach((element) => { - - if (!(element instanceof HTMLElement)) return; - if (element instanceof HTMLTemplateElement) return; - - const u = new Updater(element, object); - updaters.add(u); - - if (updaterCallbacks.length > 0) { - for (const [name, callback] of updaterCallbacks) { - u.setCallback(name, callback); - } - } - - result.push( - u.run().then(() => { - return u.enableEventProcessing(); - }), - ); - }); - - if (updaters.size > 0) { - addToObjectLink(this, symbol, updaters); - } - - return result; + if (!(this instanceof HTMLElement)) { + throw new TypeError( + "the context of this function must be an instance of HTMLElement", + ); + } + + if (!(typeof symbol === "symbol")) { + throw new TypeError("symbol must be an instance of Symbol"); + } + + const updaters = new Set(); + + if (elements instanceof NodeList) { + elements = new Set([...elements]); + } else if (elements instanceof HTMLElement) { + elements = new Set([elements]); + } else if (elements instanceof Set) { + } else { + throw new TypeError( + `elements is not a valid type. (actual: ${typeof elements})`, + ); + } + + const result = []; + + const updaterCallbacks = []; + const cb = this?.[updaterTransformerMethodsSymbol]; + if (this instanceof HTMLElement && typeof cb === "function") { + const callbacks = cb.call(this); + if (typeof callbacks === "object") { + for (const [name, callback] of Object.entries(callbacks)) { + if (typeof callback === "function") { + updaterCallbacks.push([name, callback]); + } else { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + `onUpdaterPipeCallbacks: ${name} is not a function`, + ); + } + } + } else { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + `onUpdaterPipeCallbacks do not return an object with functions`, + ); + } + } + + elements.forEach((element) => { + if (!(element instanceof HTMLElement)) return; + if (element instanceof HTMLTemplateElement) return; + + const u = new Updater(element, object); + updaters.add(u); + + if (updaterCallbacks.length > 0) { + for (const [name, callback] of updaterCallbacks) { + u.setCallback(name, callback); + } + } + + result.push( + u.run().then(() => { + return u.enableEventProcessing(); + }), + ); + }); + + if (updaters.size > 0) { + addToObjectLink(this, symbol, updaters); + } + + return result; } diff --git a/source/dom/util/set-option-from-attribute.mjs b/source/dom/util/set-option-from-attribute.mjs index 5f25721c5c419fd52b9fae1ae8554a7ed22e1745..dd717e1eee33c07715c0c9fcd97be544add3595b 100644 --- a/source/dom/util/set-option-from-attribute.mjs +++ b/source/dom/util/set-option-from-attribute.mjs @@ -49,7 +49,6 @@ function setOptionFromAttribute( mapping = {}, prefix = "data-monster-option-", ) { - if (!(element instanceof HTMLElement)) return options; if (!element.hasAttributes()) return options; @@ -85,4 +84,4 @@ function setOptionFromAttribute( finder.setVia(optionName, value); return options; -} +} diff --git a/source/monster.mjs b/source/monster.mjs index 9eb853d229da7086503b44c7ebf39e1669e7e1c0..0e43478c33b052e9ef5f79c6f26e54ffb744452d 100644 --- a/source/monster.mjs +++ b/source/monster.mjs @@ -1,4 +1,3 @@ - /** * Copyright schukai GmbH and contributors 2023. All Rights Reserved. * Node module: @schukai/monster