diff --git a/development/issues/closed/277.html b/development/issues/closed/277.html index 570eaaf080de96bd0dcf8402b916950ee180b6df..ea5bb32eb8f0870868f820cbedebdf8a47441f01 100644 --- a/development/issues/closed/277.html +++ b/development/issues/closed/277.html @@ -15,7 +15,7 @@ </ul> <main> - <monster-host><monster-config-manager></monster-config-manager></monster-host> + <!--monster-host><monster-config-manager></monster-config-manager></monster-host --> <monster-notify data-monster-option-orientation="bottom right"></monster-notify> <monster-monitor-attribute-errors data-monster-option-features-notifyUser="false" @@ -27,13 +27,14 @@ <monster-datasource-rest id="ds277" data-monster-option-features-autoInit="true" data-monster-option-write-url="/issue-274.json" - data-monster-option-read-url="/issue-274.json?limit=5&page=${page}&q=${query}" + data-monster-option-read-url="/issue-274.json?limit=5&page=${page}&q=${query}&order=${order}" data-monster-option-filter-id="ds277-filter" data-monster-option-features-filter="true" ></monster-datasource-rest> - <monster-datatable data-monster-datasource-selector="#ds277" id="dt277"> + <monster-datatable data-monster-datasource-selector="#ds277" id="dt277" + data-monster-option-datasource-orderdelimiter="::"> <monster-collapse id="filter-collapse" data-monster-role="filter-collapse"> <div class="flex"> @@ -81,11 +82,11 @@ </div> <template id="dt277-row"> - <div data-monster-grid-template="2rem" data-monster-mode="fixed" data-monster-head="id" data-monster-replace="path:dt277-row.id"></div> + <div data-monster-order-template="${field}=${direction}" data-monster-sortable="id" data-monster-grid-template="3rem" data-monster-mode="fixed" data-monster-head="id" data-monster-replace="path:dt277-row.id"></div> <div data-monster-grid-template="2rem" data-monster-mode="fixed" data-monster-features="select"></div> <div data-monster-head="username" data-monster-replace="path:dt277-row.username"></div> <div data-monster-head="email" data-monster-replace="path:dt277-row.email"></div> - <div data-monster-head="full_name" data-monster-replace="path:dt277-row.full_name"></div> + <div data-monster-sortable="full_name" data-monster-head="full_name" data-monster-replace="path:dt277-row.full_name"></div> <div data-monster-head="age" data-monster-replace="path:dt277-row.age"></div> <div data-monster-head="country" data-monster-replace="path:dt277-row.country"></div> <div data-monster-head="registered_date" data-monster-replace="path:dt277-row.registered_date"></div> diff --git a/development/templates/vite.config.mjs b/development/templates/vite.config.mjs index 2ad5a59b0e40bef58ddc068ca37fbc417af06348..67f59bf675f621f50107c701bb39f0e6f9f536c2 100644 --- a/development/templates/vite.config.mjs +++ b/development/templates/vite.config.mjs @@ -95,7 +95,10 @@ export default defineConfig({ cert: "${LOCALHOST_CERTS_DIR}/localhost.alvine.dev.crt" }, watch: { - ignored: ['**/node_modules/**'] + ignored: [ + '**/node_modules', + "**/development/mock", + ] }, debug: true, proxy: { diff --git a/nix/config/release.nix b/nix/config/release.nix index 8154d8885b2e0d29d30e661b72d692090c22d867..08b205ed559c70d38121d400b3c8800376794ddd 100644 --- a/nix/config/release.nix +++ b/nix/config/release.nix @@ -3,4 +3,4 @@ commit = "8a4a2438b30a52ae5ea89dd62fd676a5de945f7e"; name = "Monster"; mnemonic = "monster"; -} \ No newline at end of file +} diff --git a/source/components/datatable/dataset.mjs b/source/components/datatable/dataset.mjs index 79301f9fa2b8b25b8178c6d43a42f3bdecbd69a8..3097021c4412e1484729700114f330cf23921e47 100644 --- a/source/components/datatable/dataset.mjs +++ b/source/components/datatable/dataset.mjs @@ -150,8 +150,13 @@ class DataSet extends CustomElement { */ refresh() { // makes sure that handleDataSourceChanges is called - this.setOption("data", {}); - return Promise.resolve(this); + return new Promise((resolve) => { + this.setOption("data", {}); + queueMicrotask(() => { + handleDataSourceChanges.call(this); + resolve(); + }); + }); } /** diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs index c17b4752e2ba37187ec433b48d6e10d850a449cb..39b90061f13fb0b4c9c06a2562bf6fbacc3ea318 100644 --- a/source/components/datatable/datasource/rest.mjs +++ b/source/components/datatable/datasource/rest.mjs @@ -118,7 +118,7 @@ class Rest extends Datasource { * @property {string} read.method The method of the rest api * @property {Object} read.parameters The parameters of the rest api * @property {Object} read.parameters.filter The filter of the rest api - * @property {Object} read.parameters.orderBy The order by of the rest api + * @property {Object} read.parameters.order The order by of the rest api * @property {Object} read.parameters.page The page of the rest api * @property {string} read.mapping.currentPage The current page * @property {Object} write Write configuration @@ -131,7 +131,7 @@ class Rest extends Datasource { restOptions.read.parameters = { filter: null, - orderBy: null, + order: null, page: "1", }; @@ -175,10 +175,10 @@ class Rest extends Datasource { * * @param {string} page * @param {string} query - * @param {string} orderBy + * @param {string} order * @return {Rest} */ - setParameters({ page, query, orderBy }) { + setParameters({ page, query, order }) { const parameters = this.getOption("read.parameters"); if (query !== undefined) { parameters.query = `${query}`; @@ -187,7 +187,7 @@ class Rest extends Datasource { // after a query the page is set to 1, so if the page is not set, it is set to 1 if (page !== undefined) parameters.page = `${page}`; - if (orderBy !== undefined) parameters.order = `${orderBy}`; + if (order !== undefined) parameters.order = `${order}`; this.setOption("read.parameters", parameters); return this; } @@ -305,8 +305,8 @@ class Rest extends Datasource { param.page = "1"; } - if (param.orderBy === null || param.orderBy === undefined) { - param.orderBy = ""; + if (param.order === null || param.order === undefined) { + param.order = ""; } const formatter = new Formatter(param); diff --git a/source/components/datatable/datatable.mjs b/source/components/datatable/datatable.mjs index 8c5327c73a4edc801aa7741b3119b294576d09de..e86a0e3f41e757e91918bb0616c350598049ad57 100644 --- a/source/components/datatable/datatable.mjs +++ b/source/components/datatable/datatable.mjs @@ -12,79 +12,79 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {Datasource} from "./datasource.mjs"; +import { Datasource } from "./datasource.mjs"; import { - assembleMethodSymbol, - CustomElement, - registerCustomElement, - getSlottedElements, + assembleMethodSymbol, + CustomElement, + registerCustomElement, + getSlottedElements, } from "../../dom/customelement.mjs"; import { - findTargetElementFromEvent, - fireCustomEvent, + findTargetElementFromEvent, + fireCustomEvent, } from "../../dom/events.mjs"; -import {clone} from "../../util/clone.mjs"; +import { clone } from "../../util/clone.mjs"; import { - isString, - isFunction, - isInstance, - isObject, - isArray, + isString, + isFunction, + isInstance, + isObject, + isArray, } from "../../types/is.mjs"; import { - validateArray, - validateInteger, - validateObject, + validateArray, + validateInteger, + validateObject, } from "../../types/validate.mjs"; -import {Observer} from "../../types/observer.mjs"; +import { Observer } from "../../types/observer.mjs"; import { - ATTRIBUTE_DATATABLE_HEAD, - ATTRIBUTE_DATATABLE_GRID_TEMPLATE, - ATTRIBUTE_DATASOURCE_SELECTOR, - ATTRIBUTE_DATATABLE_ALIGN, - ATTRIBUTE_DATATABLE_SORTABLE, - ATTRIBUTE_DATATABLE_MODE, - ATTRIBUTE_DATATABLE_INDEX, - ATTRIBUTE_DATATABLE_MODE_HIDDEN, - ATTRIBUTE_DATATABLE_FEATURES, - ATTRIBUTE_DATATABLE_MODE_VISIBLE, - ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, - ATTRIBUTE_DATATABLE_MODE_FIXED, + ATTRIBUTE_DATATABLE_HEAD, + ATTRIBUTE_DATATABLE_GRID_TEMPLATE, + ATTRIBUTE_DATASOURCE_SELECTOR, + ATTRIBUTE_DATATABLE_ALIGN, + ATTRIBUTE_DATATABLE_SORTABLE, + ATTRIBUTE_DATATABLE_MODE, + ATTRIBUTE_DATATABLE_INDEX, + ATTRIBUTE_DATATABLE_MODE_HIDDEN, + ATTRIBUTE_DATATABLE_FEATURES, + ATTRIBUTE_DATATABLE_MODE_VISIBLE, + ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, + ATTRIBUTE_DATATABLE_MODE_FIXED, } from "./constants.mjs"; -import {instanceSymbol} from "../../constants.mjs"; +import { instanceSymbol } from "../../constants.mjs"; import { - Header, - createOrderStatement, - DIRECTION_ASC, - DIRECTION_DESC, - DIRECTION_NONE, + Header, + createOrderStatement, + DIRECTION_ASC, + DIRECTION_DESC, + DIRECTION_NONE, } from "./datatable/header.mjs"; -import {DatatableStyleSheet} from "./stylesheet/datatable.mjs"; +import { DatatableStyleSheet } from "./stylesheet/datatable.mjs"; import { - handleDataSourceChanges, - datasourceLinkedElementSymbol, + handleDataSourceChanges, + datasourceLinkedElementSymbol, } from "./util.mjs"; import "./columnbar.mjs"; import "./filter-button.mjs"; import { - findElementWithSelectorUpwards, - getDocument, - getWindow, + findElementWithSelectorUpwards, + getDocument, + getWindow, } from "../../dom/util.mjs"; -import {getDocumentTranslations} from "../../i18n/translations.mjs"; +import { getDocumentTranslations } from "../../i18n/translations.mjs"; import "../state/state.mjs"; import "../host/collapse.mjs"; -import {generateUniqueConfigKey} from "../host/util.mjs"; +import { generateUniqueConfigKey } from "../host/util.mjs"; import "./datasource/dom.mjs"; import "./datasource/rest.mjs"; import "../form/context-help.mjs"; -import {getLocaleOfDocument} from "../../dom/locale.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { getLocaleOfDocument } from "../../dom/locale.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; -export {DataTable}; +export { DataTable }; /** * @private @@ -136,6 +136,7 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * @example /examples/components/datatable/datasource Use a datasource * @example /examples/components/datatable/pagination Use pagination * @example /examples/components/datatable/filter Filer the data + * @example /examples/components/datatable/order-by Order the data * @example /examples/components/datatable/select-rows Select rows * * @copyright schukai GmbH @@ -150,421 +151,412 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * @fires monster-datatable-selection-changed **/ class DataTable extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/datatable@@instance"); - } - - /** - * 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} templates Template definitions - * @property {string} templates.main Main template - * @property {Object} datasource Datasource configuration - * @property {string} datasource.selector Selector for the datasource - * @property {Object} mapping Mapping configuration - * @property {string} mapping.data Data mapping - * @property {Array} data Data - * @property {Array} headers Headers - * @property {Object} responsive Responsive configuration - * @property {number} responsive.breakpoint Breakpoint for responsive mode - * @property {Object} labels Labels - * @property {string} labels.theListContainsNoEntries Label for empty state - * @property {Object} classes Classes - * @property {string} classes.container Container class - * @property {Object} features Features - * @property {boolean} features.settings Settings feature - * @property {boolean} features.footer Footer feature - * @property {boolean} features.autoInit Auto init feature (init datasource automatically) - * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature - * @property {boolean} features.copyAll Copy all feature - * @property {boolean} features.help Help feature - * @property {Object} templateMapping Template mapping - * @property {string} templateMapping.row-key Row key - * @property {string} templateMapping.filter-id Filter id - **/ - get defaults() { - return Object.assign( - {}, - super.defaults, - { - templates: { - main: getTemplate(), - emptyState: getEmptyTemplate(), - }, - - datasource: { - selector: null, - }, - - mapping: { - data: "dataset", - }, - - data: [], - headers: [], - - responsive: { - breakpoint: 900, - }, - - labels: getTranslations(), - - classes: { - control: "monster-theme-control-container-1", - container: "", - row: "monster-theme-control-row-1", - }, - - features: { - settings: true, - footer: true, - autoInit: true, - doubleClickCopyToClipboard: true, - copyAll: true, - help: true, - }, - - copy: { - delimiter: ";", - quoteOpen: '"', - quoteClose: '"', - rowBreak: "\n", - }, - - templateMapping: { - "row-key": null, - "filter-id": null, - }, - }, - initOptionsFromArguments.call(this), - ); - } - - /** - * - * @param {string} selector - * @return {NodeList} - */ - getGridElements(selector) { - return this[gridElementSymbol].querySelectorAll(selector); - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-datatable"; - } - - /** - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - if (this?.[resizeObserverSymbol] instanceof ResizeObserver) { - this[resizeObserverSymbol].disconnect(); - } - } - - /** - * @return {void} - */ - connectedCallback() { - const self = this; - super.connectedCallback(); - - this[resizeObserverSymbol] = new ResizeObserver((entries) => { - updateGrid.call(self); - }); - - this[resizeObserverSymbol].observe(this.parentNode); - } - - /** - * Get the row number of the selected rows as an array - * - * @returns {number[]} - */ - getSelectedRows() { - const rows = this.getGridElements(`[data-monster-role="select-row"]`); - const selectedRows = []; - rows.forEach((row) => { - if (row.checked) { - const key = row.parentNode.getAttribute( - "data-monster-insert-reference", - ); - const index = key.split("-").pop(); - selectedRows.push(parseInt(index, 10)); - } - }); - - return selectedRows; - } - - /** - * @return void - */ - [assembleMethodSymbol]() { - const rawKey = this.getOption("templateMapping.row-key"); - - if (rawKey === null) { - if (this.id !== null && this.id !== "") { - const rawKey = this.getOption("templateMapping.row-key"); - if (rawKey === null) { - this.setOption("templateMapping.row-key", this.id + "-row"); - } - } else { - this.setOption("templateMapping.row-key", "row"); - } - } - - if (this.id !== null && this.id !== "") { - this.setOption("templateMapping.filter-id", "" + this.id + "-filter"); - } else { - this.setOption("templateMapping.filter-id", "filter"); - } - - super[assembleMethodSymbol](); - - initControlReferences.call(this); - initEventHandler.call(this); - - getHostConfig - .call(this, getColumnVisibilityConfigKey) - .then((config) => { - const headerOrderMap = new Map(); - - getHostConfig - .call(this, getStoredOrderConfigKey) - .then((orderConfig) => { - if (isArray(orderConfig) || orderConfig.length > 0) { - for (let i = 0; i < orderConfig.length; i++) { - const item = orderConfig[i]; - const parts = item.split(" "); - const field = parts[0]; - const direction = parts[1] || DIRECTION_ASC; - headerOrderMap.set(field, direction); - } - } - }) - .then(() => { - try { - initGridAndStructs.call(this, config, headerOrderMap); - } catch (error) { - addErrorAttribute( - this, - error - ); - } - - updateColumnBar.call(this); - }) - .catch((error) => { - addErrorAttribute( - this, error, - ); - }); - }) - .catch((error) => { - addErrorAttribute( - this, - error - ); - }) - .finally(() => { - - const selector = this.getOption("datasource.selector"); - - if (isString(selector)) { - const element = findElementWithSelectorUpwards(this, selector); - if (element === null) { - throw new Error("the selector must match exactly one element"); - } - - if (!isInstance(element, Datasource)) { - throw new TypeError("the element must be a datasource"); - } - - this[datasourceLinkedElementSymbol] = element; - - getWindow().requestAnimationFrame(() => { - - handleDataSourceChanges.call(this); - if (element && "datasource" in element) { - element.datasource.attachObserver( - new Observer(handleDataSourceChanges.bind(this)), - ); - } - }); - } - }); - - } - - /** - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [DatatableStyleSheet]; - } - - /** - * Copy a row from the datatable - * - * @param {number|string} fromIndex - * @param {number|string} toIndex - * @return {DataTable} - * @fires monster-datatable-row-copied - */ - copyRow(fromIndex, toIndex) { - const datasource = this[datasourceLinkedElementSymbol]; - if (!datasource) { - return this; - } - let d = datasource.data; - let c = clone(d); - - let rows = c; - const mapping = this.getOption("mapping.data"); - - if (mapping) { - rows = c?.[mapping]; - } - - if (rows === undefined || rows === null) { - rows = []; - } - - if (toIndex === undefined) { - toIndex = rows.length; - } - - if (isString(fromIndex)) { - fromIndex = parseInt(fromIndex); - } - if (isString(toIndex)) { - toIndex = parseInt(toIndex); - } - - if (toIndex < 0 || toIndex > rows.length) { - throw new RangeError("index out of bounds"); - } - - validateArray(rows); - validateInteger(fromIndex); - validateInteger(toIndex); - - if (fromIndex < 0 || fromIndex >= rows.length) { - throw new RangeError("index out of bounds"); - } - - rows.splice(toIndex, 0, clone(rows[fromIndex])); - datasource.data = c; - - fireCustomEvent(this, "monster-datatable-row-copied", { - index: toIndex, - }); - - return this; - } - - /** - * Remove a row from the datatable - * - * @param {number|string} index - * @return {DataTable} - * @fires monster-datatable-row-removed - */ - removeRow(index) { - const datasource = this[datasourceLinkedElementSymbol]; - if (!datasource) { - return this; - } - let d = datasource.data; - let c = clone(d); - - let rows = c; - const mapping = this.getOption("mapping.data"); - - if (mapping) { - rows = c?.[mapping]; - } - - if (rows === undefined || rows === null) { - rows = []; - } - - if (isString(index)) { - index = parseInt(index); - } - - validateArray(rows); - validateInteger(index); - - if (index < 0 || index >= rows.length) { - throw new RangeError("index out of bounds"); - } - if (mapping) { - rows = c?.[mapping]; - } - - rows.splice(index, 1); - datasource.data = c; - - fireCustomEvent(this, "monster-datatable-row-removed", { - index: index, - }); - - return this; - } - - /** - * Add a row to the datatable - * - * @param {Object} data - * @return {DataTable} - * - * @fires monster-datatable-row-added - **/ - addRow(data) { - const datasource = this[datasourceLinkedElementSymbol]; - if (!datasource) { - return this; - } - let d = datasource.data; - let c = clone(d); - - let rows = c; - - const mapping = this.getOption("mapping.data"); - if (mapping) { - rows = c?.[mapping]; - } - - if (rows === undefined || rows === null) { - rows = []; - } - - validateArray(rows); - validateObject(data); - - rows.push(data); - datasource.data = c; - - fireCustomEvent(this, "monster-datatable-row-added", { - index: rows.length - 1, - }); - - return this; - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/datatable@@instance"); + } + + /** + * 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} templates Template definitions + * @property {string} templates.main Main template + * @property {Object} datasource Datasource configuration + * @property {string} datasource.selector Selector for the datasource + * @property {string} datasource.orderDelimiter Order delimiter + * @property {Object} mapping Mapping configuration + * @property {string} mapping.data Data mapping + * @property {Array} data Data + * @property {Array} headers Headers + * @property {Object} responsive Responsive configuration + * @property {number} responsive.breakpoint Breakpoint for responsive mode + * @property {Object} labels Labels + * @property {string} labels.theListContainsNoEntries Label for empty state + * @property {Object} classes Classes + * @property {string} classes.container Container class + * @property {Object} features Features + * @property {boolean} features.settings Settings feature + * @property {boolean} features.footer Footer feature + * @property {boolean} features.autoInit Auto init feature (init datasource automatically) + * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature + * @property {boolean} features.copyAll Copy all feature + * @property {boolean} features.help Help feature + * @property {Object} templateMapping Template mapping + * @property {string} templateMapping.row-key Row key + * @property {string} templateMapping.filter-id Filter id + **/ + get defaults() { + return Object.assign( + {}, + super.defaults, + { + templates: { + main: getTemplate(), + emptyState: getEmptyTemplate(), + }, + + datasource: { + selector: null, + orderDelimiter: ",", // look at initOptionsFromArguments() + }, + + mapping: { + data: "dataset", + }, + + data: [], + headers: [], + + responsive: { + breakpoint: 900, + }, + + labels: getTranslations(), + + classes: { + control: "monster-theme-control-container-1", + container: "", + row: "monster-theme-control-row-1", + }, + + features: { + settings: true, + footer: true, + autoInit: true, + doubleClickCopyToClipboard: true, + copyAll: true, + help: true, + }, + + copy: { + delimiter: ";", + quoteOpen: '"', + quoteClose: '"', + rowBreak: "\n", + }, + + templateMapping: { + "row-key": null, + "filter-id": null, + }, + }, + initOptionsFromArguments.call(this), + ); + } + + /** + * + * @param {string} selector + * @return {NodeList} + */ + getGridElements(selector) { + return this[gridElementSymbol].querySelectorAll(selector); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-datatable"; + } + + /** + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + if (this?.[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } + } + + /** + * @return {void} + */ + connectedCallback() { + const self = this; + super.connectedCallback(); + + this[resizeObserverSymbol] = new ResizeObserver((entries) => { + updateGrid.call(self); + }); + + this[resizeObserverSymbol].observe(this.parentNode); + } + + /** + * Get the row number of the selected rows as an array + * + * @returns {number[]} + */ + getSelectedRows() { + const rows = this.getGridElements(`[data-monster-role="select-row"]`); + const selectedRows = []; + rows.forEach((row) => { + if (row.checked) { + const key = row.parentNode.getAttribute( + "data-monster-insert-reference", + ); + const index = key.split("-").pop(); + selectedRows.push(parseInt(index, 10)); + } + }); + + return selectedRows; + } + + /** + * @return void + */ + [assembleMethodSymbol]() { + const rawKey = this.getOption("templateMapping.row-key"); + + if (rawKey === null) { + if (this.id !== null && this.id !== "") { + const rawKey = this.getOption("templateMapping.row-key"); + if (rawKey === null) { + this.setOption("templateMapping.row-key", this.id + "-row"); + } + } else { + this.setOption("templateMapping.row-key", "row"); + } + } + + if (this.id !== null && this.id !== "") { + this.setOption("templateMapping.filter-id", "" + this.id + "-filter"); + } else { + this.setOption("templateMapping.filter-id", "filter"); + } + + super[assembleMethodSymbol](); + + initControlReferences.call(this); + initEventHandler.call(this); + + getHostConfig + .call(this, getColumnVisibilityConfigKey) + .then((config) => { + const headerOrderMap = new Map(); + + getHostConfig + .call(this, getStoredOrderConfigKey) + .then((orderConfig) => { + if (isArray(orderConfig) || orderConfig.length > 0) { + for (let i = 0; i < orderConfig.length; i++) { + const item = orderConfig[i]; + const parts = item.split(" "); + const field = parts[0]; + const direction = parts[1] || DIRECTION_ASC; + headerOrderMap.set(field, direction); + } + } + }) + .then(() => { + try { + initGridAndStructs.call(this, config, headerOrderMap); + } catch (error) { + addErrorAttribute(this, error); + } + + updateColumnBar.call(this); + }) + .catch((error) => { + addErrorAttribute(this, error); + }); + }) + .catch((error) => { + addErrorAttribute(this, error); + }) + .finally(() => { + const selector = this.getOption("datasource.selector"); + + if (isString(selector)) { + const element = findElementWithSelectorUpwards(this, selector); + if (element === null) { + throw new Error("the selector must match exactly one element"); + } + + if (!isInstance(element, Datasource)) { + throw new TypeError("the element must be a datasource"); + } + + this[datasourceLinkedElementSymbol] = element; + + getWindow().requestAnimationFrame(() => { + handleDataSourceChanges.call(this); + if (element && "datasource" in element) { + element.datasource.attachObserver( + new Observer(handleDataSourceChanges.bind(this)), + ); + } + }); + } + }); + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [DatatableStyleSheet]; + } + + /** + * Copy a row from the datatable + * + * @param {number|string} fromIndex + * @param {number|string} toIndex + * @return {DataTable} + * @fires monster-datatable-row-copied + */ + copyRow(fromIndex, toIndex) { + const datasource = this[datasourceLinkedElementSymbol]; + if (!datasource) { + return this; + } + let d = datasource.data; + let c = clone(d); + + let rows = c; + const mapping = this.getOption("mapping.data"); + + if (mapping) { + rows = c?.[mapping]; + } + + if (rows === undefined || rows === null) { + rows = []; + } + + if (toIndex === undefined) { + toIndex = rows.length; + } + + if (isString(fromIndex)) { + fromIndex = parseInt(fromIndex); + } + if (isString(toIndex)) { + toIndex = parseInt(toIndex); + } + + if (toIndex < 0 || toIndex > rows.length) { + throw new RangeError("index out of bounds"); + } + + validateArray(rows); + validateInteger(fromIndex); + validateInteger(toIndex); + + if (fromIndex < 0 || fromIndex >= rows.length) { + throw new RangeError("index out of bounds"); + } + + rows.splice(toIndex, 0, clone(rows[fromIndex])); + datasource.data = c; + + fireCustomEvent(this, "monster-datatable-row-copied", { + index: toIndex, + }); + + return this; + } + + /** + * Remove a row from the datatable + * + * @param {number|string} index + * @return {DataTable} + * @fires monster-datatable-row-removed + */ + removeRow(index) { + const datasource = this[datasourceLinkedElementSymbol]; + if (!datasource) { + return this; + } + let d = datasource.data; + let c = clone(d); + + let rows = c; + const mapping = this.getOption("mapping.data"); + + if (mapping) { + rows = c?.[mapping]; + } + + if (rows === undefined || rows === null) { + rows = []; + } + + if (isString(index)) { + index = parseInt(index); + } + + validateArray(rows); + validateInteger(index); + + if (index < 0 || index >= rows.length) { + throw new RangeError("index out of bounds"); + } + if (mapping) { + rows = c?.[mapping]; + } + + rows.splice(index, 1); + datasource.data = c; + + fireCustomEvent(this, "monster-datatable-row-removed", { + index: index, + }); + + return this; + } + + /** + * Add a row to the datatable + * + * @param {Object} data + * @return {DataTable} + * + * @fires monster-datatable-row-added + **/ + addRow(data) { + const datasource = this[datasourceLinkedElementSymbol]; + if (!datasource) { + return this; + } + let d = datasource.data; + let c = clone(d); + + let rows = c; + + const mapping = this.getOption("mapping.data"); + if (mapping) { + rows = c?.[mapping]; + } + + if (rows === undefined || rows === null) { + rows = []; + } + + validateArray(rows); + validateObject(data); + + rows.push(data); + datasource.data = c; + + fireCustomEvent(this, "monster-datatable-row-added", { + index: rows.length - 1, + }); + + return this; + } } /** @@ -572,7 +564,7 @@ class DataTable extends CustomElement { * @return {string} */ function getColumnVisibilityConfigKey() { - return generateUniqueConfigKey("datatable", this?.id, "columns-visibility"); + return generateUniqueConfigKey("datatable", this?.id, "columns-visibility"); } /** @@ -580,7 +572,7 @@ function getColumnVisibilityConfigKey() { * @return {string} */ function getFilterConfigKey() { - return generateUniqueConfigKey("datatable", this?.id, "filter"); + return generateUniqueConfigKey("datatable", this?.id, "filter"); } /** @@ -588,537 +580,530 @@ function getFilterConfigKey() { * @return {Promise} */ function getHostConfig(callback) { - const host = findElementWithSelectorUpwards(this, "monster-host"); - - if (!host) { - addErrorAttribute(this, "no host found"); - return Promise.resolve({}); - } - - if (!this.id) { - addErrorAttribute( - this, - "no id found; id is required for config", - ); - return Promise.resolve({}); - } - - if (!host || !isFunction(host?.getConfig)) { - throw new TypeError("the host must be a monster-host"); - } - - const configKey = callback.call(this); - return host.hasConfig(configKey).then((hasConfig) => { - if (hasConfig) { - return host.getConfig(configKey); - } else { - return {}; - } - }); + const host = findElementWithSelectorUpwards(this, "monster-host"); + + if (!host) { + addErrorAttribute(this, "no host found"); + return Promise.resolve({}); + } + + if (!this.id) { + addErrorAttribute(this, "no id found; id is required for config"); + return Promise.resolve({}); + } + + if (!host || !isFunction(host?.getConfig)) { + throw new TypeError("the host must be a monster-host"); + } + + const configKey = callback.call(this); + return host.hasConfig(configKey).then((hasConfig) => { + if (hasConfig) { + return host.getConfig(configKey); + } else { + return {}; + } + }); } /** * @private */ function updateColumnBar() { - if (!this[columnBarElementSymbol]) { - return; - } - - const columns = []; - for (const header of this.getOption("headers")) { - const mode = header.getInternal("mode"); - - if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { - continue; - } - - columns.push({ - visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN, - name: header.label, - index: header.index, - }); - } - - this[columnBarElementSymbol].setOption("columns", columns); + if (!this[columnBarElementSymbol]) { + return; + } + + const columns = []; + for (const header of this.getOption("headers")) { + const mode = header.getInternal("mode"); + + if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { + continue; + } + + columns.push({ + visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN, + name: header.label, + index: header.index, + }); + } + + this[columnBarElementSymbol].setOption("columns", columns); } /** * @private */ function updateHeaderFromColumnBar() { - if (!this[columnBarElementSymbol]) { - return; - } + if (!this[columnBarElementSymbol]) { + return; + } - const options = this[columnBarElementSymbol].getOption("columns"); - if (!isArray(options)) return; + const options = this[columnBarElementSymbol].getOption("columns"); + if (!isArray(options)) return; - const invisibleMap = {}; + const invisibleMap = {}; - for (let i = 0; i < options.length; i++) { - const option = options[i]; - invisibleMap[option.index] = option.visible; - } + for (let i = 0; i < options.length; i++) { + const option = options[i]; + invisibleMap[option.index] = option.visible; + } - for (const header of this.getOption("headers")) { - const mode = header.getInternal("mode"); + for (const header of this.getOption("headers")) { + const mode = header.getInternal("mode"); - if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { - continue; - } + if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) { + continue; + } - if (invisibleMap[header.index] === false) { - header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN); - } else { - header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE); - } - } + if (invisibleMap[header.index] === false) { + header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN); + } else { + header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE); + } + } } /** * @private */ function updateConfigColumnBar() { - if (!this[columnBarElementSymbol]) { - return; - } - - const options = this[columnBarElementSymbol].getOption("columns"); - if (!isArray(options)) return; - - const map = {}; - for (let i = 0; i < options.length; i++) { - const option = options[i]; - map[option.name] = option.visible; - } - - const host = findElementWithSelectorUpwards(this, "monster-host"); - if (!(host && this.id)) { - return; - } - const configKey = getColumnVisibilityConfigKey.call(this); - - try { - host.setConfig(configKey, map); - } catch (error) { - addErrorAttribute(this, error); - } + if (!this[columnBarElementSymbol]) { + return; + } + + const options = this[columnBarElementSymbol].getOption("columns"); + if (!isArray(options)) return; + + const map = {}; + for (let i = 0; i < options.length; i++) { + const option = options[i]; + map[option.name] = option.visible; + } + + const host = findElementWithSelectorUpwards(this, "monster-host"); + if (!(host && this.id)) { + return; + } + const configKey = getColumnVisibilityConfigKey.call(this); + + try { + host.setConfig(configKey, map); + } catch (error) { + addErrorAttribute(this, error); + } } /** * @private */ function initEventHandler() { - const self = this; - - const quoteOpenChar = this.getOption("copy.quoteOpen"); - const quoteCloseChar = this.getOption("copy.quoteClose"); - const delimiterChar = this.getOption("copy.delimiter"); - const rowBreak = this.getOption("copy.rowBreak"); - - self[columnBarElementSymbol].attachObserver( - new Observer((e) => { - updateHeaderFromColumnBar.call(self); - updateGrid.call(self); - updateConfigColumnBar.call(self); - }), - ); - - self[gridHeadersElementSymbol].addEventListener("click", function (event) { - let element = null; - const datasource = self[datasourceLinkedElementSymbol]; - if (!datasource) { - return; - } - - element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE); - if (element) { - const index = element.parentNode.getAttribute(ATTRIBUTE_DATATABLE_INDEX); - const headers = self.getOption("headers"); - - event.preventDefault(); - - headers[index].changeDirection(); - - queueMicrotask(function () { - /** hotfix, normally this should be done via the updater, no idea why this is not possible. */ - element.setAttribute( - ATTRIBUTE_DATATABLE_SORTABLE, - `${headers[index].field} ${headers[index].direction}`, - ); - - storeOrderStatement.call(self, true); - }); - } - }); - - const eventHandlerDoubleClickCopyToClipboard = (event) => { - const element = findTargetElementFromEvent(event, "data-monster-head"); - if (element) { - let text = ""; - - if (event.shiftKey) { - const index = element.getAttribute("data-monster-insert-reference"); - if (index) { - const cols = self.getGridElements( - `[data-monster-insert-reference="${index}"]`, - ); - - const colTexts = []; - for (let i = 0; i < cols.length; i++) { - const col = cols[i]; - - if ( - col.querySelector("monster-button-bar") || - col.querySelector("monster-button") - ) { - continue; - } - - if (col.textContent) { - colTexts.push( - quoteOpenChar + col.textContent.trim() + quoteCloseChar, - ); - } - } - - text = colTexts.join(delimiterChar); - } - } else { - if ( - element.querySelector("monster-button-bar") || - element.querySelector("monster-button") - ) { - return; - } - - text = element.textContent.trim(); - } - - if (getWindow().navigator.clipboard && text) { - getWindow() - .navigator.clipboard.writeText(text) - .then( - () => { - }, - (err) => { - }, - ); - } - } - }; - - if (self.getOption("features.doubleClickCopyToClipboard")) { - self[gridElementSymbol].addEventListener( - "dblclick", - eventHandlerDoubleClickCopyToClipboard, - ); - } - - if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) { - this[copyAllElementSymbol].addEventListener("click", (event) => { - event.preventDefault(); - - const table = []; - let currentRow = []; - let currentIndex = null; - - const cols = self.getGridElements(`[data-monster-insert-reference]`); - const rowIndexes = new Map(); - cols.forEach((col) => { - const index = col.getAttribute("data-monster-insert-reference"); - rowIndexes.set(index, true); - }); - - rowIndexes.forEach((value, key) => { - const cols = self.getGridElements( - `[data-monster-insert-reference="${key}"]`, - ); - - for (let i = 0; i < cols.length; i++) { - const col = cols[i]; - - if ( - col.querySelector("monster-button-bar") || - col.querySelector("monster-button") - ) { - continue; - } - - if (col.textContent) { - currentRow.push( - quoteOpenChar + col.textContent.trim() + quoteCloseChar, - ); - } - } - - if (currentRow.length > 0) { - table.push(currentRow); - } - currentRow = []; - }); - - if (table.length > 0) { - const text = table.map((row) => row.join(delimiterChar)).join(rowBreak); - if (getWindow().navigator.clipboard && text) { - getWindow() - .navigator.clipboard.writeText(text) - .then( - () => { - }, - (err) => { - }, - ); - } - } - }); - } - - const selectRowCallback = (event) => { - const element = findTargetElementFromEvent( - event, - "data-monster-role", - "select-row", - ); - - if (!element) { - return; - } - - const key = element.parentNode.getAttribute( - "data-monster-insert-reference", - ); - const row = self.getGridElements( - `[data-monster-insert-reference="${key}"]`, - ); - - const index = key.split("-").pop(); - - if (element.checked) { - row.forEach((col) => { - col.classList.add("selected"); - }); - - fireCustomEvent(self, "monster-datatable-row-selected", { - index: index, - }); - } else { - row.forEach((col) => { - col.classList.remove("selected"); - }); - - fireCustomEvent(self, "monster-datatable-row-deselected", { - index: index, - }); - } - - fireCustomEvent(this, "monster-datatable-selection-changed", {}); - - const rows = self.getGridElements(`[data-monster-role="select-row"]`); - const allSelected = Array.from(rows).every((row) => row.checked); - - const selectAll = this[gridHeadersElementSymbol].querySelector( - `[data-monster-role="select-all"]`, - ); - - if (selectAll) { - selectAll.checked = allSelected; - } - - }; - - this[gridElementSymbol].addEventListener("click", selectRowCallback); - this[gridElementSymbol].addEventListener("touch", selectRowCallback); - - const selectAllCallback = (event) => { - const element = findTargetElementFromEvent( - event, - "data-monster-role", - "select-all", - ); - if (element) { - const mode = element.checked; - - const rows = this.getGridElements(`[data-monster-role="select-row"]`); - rows.forEach((row) => { - row.checked = mode; - }); - - if (mode) { - fireCustomEvent(this, "monster-datatable-all-rows-selected", {}); - } else { - fireCustomEvent(this, "monster-datatable-all-rows-deselected", {}); - } - - fireCustomEvent(this, "monster-datatable-selection-changed", {}); - } - }; - - this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback); - this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback); + const self = this; + + const quoteOpenChar = this.getOption("copy.quoteOpen"); + const quoteCloseChar = this.getOption("copy.quoteClose"); + const delimiterChar = this.getOption("copy.delimiter"); + const rowBreak = this.getOption("copy.rowBreak"); + + self[columnBarElementSymbol].attachObserver( + new Observer((e) => { + updateHeaderFromColumnBar.call(self); + updateGrid.call(self); + updateConfigColumnBar.call(self); + }), + ); + + self[gridHeadersElementSymbol].addEventListener("click", function (event) { + let element = null; + const datasource = self[datasourceLinkedElementSymbol]; + if (!datasource) { + return; + } + + element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE); + if (element instanceof HTMLElement) { + const index = element.parentNode?.getAttribute(ATTRIBUTE_DATATABLE_INDEX); + const headers = self.getOption("headers"); + + event.preventDefault(); + + headers[index].changeDirection(); + + queueMicrotask(function () { + /** hotfix, normally this should be done via the updater, no idea why this is not possible. */ + element.setAttribute( + ATTRIBUTE_DATATABLE_SORTABLE, + `${headers[index].field} ${headers[index].direction}`, + ); + + storeOrderStatement.call(self, true); + }); + } + }); + + const eventHandlerDoubleClickCopyToClipboard = (event) => { + const element = findTargetElementFromEvent(event, "data-monster-head"); + if (element) { + let text = ""; + + if (event.shiftKey) { + const index = element.getAttribute("data-monster-insert-reference"); + if (index) { + const cols = self.getGridElements( + `[data-monster-insert-reference="${index}"]`, + ); + + const colTexts = []; + for (let i = 0; i < cols.length; i++) { + const col = cols[i]; + + if ( + col.querySelector("monster-button-bar") || + col.querySelector("monster-button") + ) { + continue; + } + + if (col.textContent) { + colTexts.push( + quoteOpenChar + col.textContent.trim() + quoteCloseChar, + ); + } + } + + text = colTexts.join(delimiterChar); + } + } else { + if ( + element.querySelector("monster-button-bar") || + element.querySelector("monster-button") + ) { + return; + } + + text = element.textContent.trim(); + } + + if (getWindow().navigator.clipboard && text) { + getWindow() + .navigator.clipboard.writeText(text) + .then( + () => {}, + (err) => {}, + ); + } + } + }; + + if (self.getOption("features.doubleClickCopyToClipboard")) { + self[gridElementSymbol].addEventListener( + "dblclick", + eventHandlerDoubleClickCopyToClipboard, + ); + } + + if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) { + this[copyAllElementSymbol].addEventListener("click", (event) => { + event.preventDefault(); + + const table = []; + let currentRow = []; + let currentIndex = null; + + const cols = self.getGridElements(`[data-monster-insert-reference]`); + const rowIndexes = new Map(); + cols.forEach((col) => { + const index = col.getAttribute("data-monster-insert-reference"); + rowIndexes.set(index, true); + }); + + rowIndexes.forEach((value, key) => { + const cols = self.getGridElements( + `[data-monster-insert-reference="${key}"]`, + ); + + for (let i = 0; i < cols.length; i++) { + const col = cols[i]; + + if ( + col.querySelector("monster-button-bar") || + col.querySelector("monster-button") + ) { + continue; + } + + if (col.textContent) { + currentRow.push( + quoteOpenChar + col.textContent.trim() + quoteCloseChar, + ); + } + } + + if (currentRow.length > 0) { + table.push(currentRow); + } + currentRow = []; + }); + + if (table.length > 0) { + const text = table.map((row) => row.join(delimiterChar)).join(rowBreak); + if (getWindow().navigator.clipboard && text) { + getWindow() + .navigator.clipboard.writeText(text) + .then( + () => {}, + (err) => {}, + ); + } + } + }); + } + + const selectRowCallback = (event) => { + const element = findTargetElementFromEvent( + event, + "data-monster-role", + "select-row", + ); + + if (!element) { + return; + } + + const key = element.parentNode.getAttribute( + "data-monster-insert-reference", + ); + const row = self.getGridElements( + `[data-monster-insert-reference="${key}"]`, + ); + + const index = key.split("-").pop(); + + if (element.checked) { + row.forEach((col) => { + col.classList.add("selected"); + }); + + fireCustomEvent(self, "monster-datatable-row-selected", { + index: index, + }); + } else { + row.forEach((col) => { + col.classList.remove("selected"); + }); + + fireCustomEvent(self, "monster-datatable-row-deselected", { + index: index, + }); + } + + fireCustomEvent(this, "monster-datatable-selection-changed", {}); + + const rows = self.getGridElements(`[data-monster-role="select-row"]`); + const allSelected = Array.from(rows).every((row) => row.checked); + + const selectAll = this[gridHeadersElementSymbol].querySelector( + `[data-monster-role="select-all"]`, + ); + + if (selectAll) { + selectAll.checked = allSelected; + } + }; + + this[gridElementSymbol].addEventListener("click", selectRowCallback); + this[gridElementSymbol].addEventListener("touch", selectRowCallback); + + const selectAllCallback = (event) => { + const element = findTargetElementFromEvent( + event, + "data-monster-role", + "select-all", + ); + if (element) { + const mode = element.checked; + + const rows = this.getGridElements(`[data-monster-role="select-row"]`); + rows.forEach((row) => { + row.checked = mode; + }); + + if (mode) { + fireCustomEvent(this, "monster-datatable-all-rows-selected", {}); + } else { + fireCustomEvent(this, "monster-datatable-all-rows-deselected", {}); + } + + fireCustomEvent(this, "monster-datatable-selection-changed", {}); + } + }; + + this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback); + this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback); } /** * @private */ function initGridAndStructs(hostConfig, headerOrderMap) { - const rowID = this.getOption("templateMapping.row-key"); - - if (!this[gridElementSymbol]) { - addErrorAttribute(this, "no grid element found"); - return; - } - - let template; - getSlottedElements.call(this).forEach((e) => { - if (e instanceof HTMLTemplateElement && e.id === rowID) { - template = e; - } - }); - - if (!template) { - addErrorAttribute( - this, - "no template found, please add a template", - ); - return; - } - - const rowCount = template.content.children.length; - - const headers = []; - - for (let i = 0; i < rowCount; i++) { - let hClass = ""; - const row = template.content.children[i]; - - let mode = ""; - if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) { - mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE); - } - - let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE); - if (!grid || grid === "" || grid === "auto") { - grid = "minmax(0, 1fr)"; - } - - let label = ""; - let labelKey = ""; - - if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) { - label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD); - labelKey = label; - - try { - if (label.startsWith("i18n:")) { - label = label.substring(5, label.length); - label = getDocumentTranslations().getText(label, label); - } - } catch (e) { - label = "i18n error " + label; - } - } - - if (!label) { - label = i + 1 + ""; - mode = ATTRIBUTE_DATATABLE_MODE_FIXED; - labelKey = label; - } - - if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) { - if (hostConfig[label] === false) { - mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN; - } else { - mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE; - } - } - - let align = ""; - if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) { - align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN); - } - - switch (align) { - case "center": - hClass = "flex-center"; - break; - case "end": - hClass = "flex-end"; - break; - case "start": - hClass = "flex-start"; - break; - default: - hClass = "flex-start"; - } - - let field = ""; - let direction = DIRECTION_NONE; - if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) { - field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim(); - const parts = field.split(" ").map((item) => item.trim()); - field = parts[0]; - - if (headerOrderMap.has(field)) { - direction = headerOrderMap.get(field); - } else if ( - parts.length === 2 && - [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1 - ) { - direction = parts[1]; - } - } - - if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { - hClass += " hidden"; - } - - const features = []; - - if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) { - const fl = row - .getAttribute(ATTRIBUTE_DATATABLE_FEATURES) - .split(" "); - - fl.forEach((feature) => { - features.push(feature.trim()); - - if (feature === "select") { - label = "<input type='checkbox' data-monster-role='select-all' />"; - - while (row.firstChild) { - row.removeChild(row.firstChild); - } - - const checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.setAttribute("data-monster-role", "select-row"); - row.appendChild(checkbox); - } - }); - } - - const header = new Header(); - header.setInternals({ - field: field, - label: label, - classes: hClass, - index: i, - mode: mode, - grid: grid, - labelKey: labelKey, - direction: direction, - features: features, - }); - - headers.push(header); - } - - this.setOption("headers", headers); - queueMicrotask(() => { - storeOrderStatement.call(this, this.getOption("features.autoInit")); - }); + const rowID = this.getOption("templateMapping.row-key"); + + if (!this[gridElementSymbol]) { + addErrorAttribute(this, "no grid element found"); + return; + } + + let template; + getSlottedElements.call(this).forEach((e) => { + if (e instanceof HTMLTemplateElement && e.id === rowID) { + template = e; + } + }); + + if (!template) { + addErrorAttribute(this, "no template found, please add a template"); + return; + } + + const rowCount = template.content.children.length; + + const headers = []; + + for (let i = 0; i < rowCount; i++) { + let hClass = ""; + const row = template.content.children[i]; + + let mode = ""; + if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) { + mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE); + } + + let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE); + if (!grid || grid === "" || grid === "auto") { + grid = "minmax(0, 1fr)"; + } + + let label = ""; + let labelKey = ""; + + if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) { + label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD); + labelKey = label; + + try { + if (label.startsWith("i18n:")) { + label = label.substring(5, label.length); + label = getDocumentTranslations().getText(label, label); + } + } catch (e) { + label = "i18n error " + label; + } + } + + if (!label) { + label = i + 1 + ""; + mode = ATTRIBUTE_DATATABLE_MODE_FIXED; + labelKey = label; + } + + if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) { + if (hostConfig[label] === false) { + mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN; + } else { + mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE; + } + } + + let align = ""; + if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) { + align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN); + } + + switch (align) { + case "center": + hClass = "flex-center"; + break; + case "end": + hClass = "flex-end"; + break; + case "start": + hClass = "flex-start"; + break; + default: + hClass = "flex-start"; + } + + let field = ""; + let direction = DIRECTION_NONE; + if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) { + field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim(); + const parts = field.split(" ").map((item) => item.trim()); + field = parts[0]; + + if (headerOrderMap.has(field)) { + direction = headerOrderMap.get(field); + } else if ( + parts.length === 2 && + [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1 + ) { + direction = parts[1]; + } + } + + if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { + hClass += " hidden"; + } + + const features = []; + + if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) { + const fl = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" "); + + fl.forEach((feature) => { + features.push(feature.trim()); + + if (feature === "select") { + label = "<input type='checkbox' data-monster-role='select-all' />"; + + while (row.firstChild) { + row.removeChild(row.firstChild); + } + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.setAttribute("data-monster-role", "select-row"); + row.appendChild(checkbox); + } + }); + } + + let orderTemplate = "${field} ${direction}"; + if (row.hasAttribute("data-monster-order-template")) { + orderTemplate = row.getAttribute("data-monster-order-template"); + } + + const header = new Header(); + header.setInternals({ + field: field, + label: label, + classes: hClass, + index: i, + mode: mode, + grid: grid, + labelKey: labelKey, + direction: direction, + features: features, + orderTemplate: orderTemplate + }); + + headers.push(header); + } + + this.setOption("headers", headers); + queueMicrotask(() => { + storeOrderStatement.call(this, this.getOption("features.autoInit")); + }); } /** @@ -1126,101 +1111,101 @@ function initGridAndStructs(hostConfig, headerOrderMap) { * @returns {object} */ function getTranslations() { - const locale = getLocaleOfDocument(); - switch (locale.language) { - case "de": - return { - theListContainsNoEntries: "Die Liste enthält keine Einträge", - copyAll: "Alles kopieren", - helpText: - "<p>Sie können die Werte aus einzelnen Zeilen<br>" + - "in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" + - "<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" + - "Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>", - }; - case "fr": - return { - theListContainsNoEntries: "La liste ne contient aucune entrée", - copyAll: "Copier tout", - helpText: - "<p>Vous pouvez copier les valeurs des rangées individuelles<br>" + - "dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" + - "<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" + - "Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>", - }; - case "sp": - return { - theListContainsNoEntries: "La lista no contiene entradas", - copyAll: "Copiar todo", - helpText: - "<p>Puedes copiar los valores de filas individuales<br>" + - "al portapapeles haciendo doble clic en la columna correspondiente.</p>" + - "<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" + - "Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>", - }; - case "it": - return { - theListContainsNoEntries: "L'elenco non contiene voci", - copyAll: "Copia tutto", - helpText: - "<p>Puoi copiare i valori dalle singole righe<br>" + - "negli appunti facendo doppio clic sulla colonna relativa.</p>" + - "<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" + - "Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>", - }; - case "pl": - return { - theListContainsNoEntries: "Lista nie zawiera wpisów", - copyAll: "Kopiuj wszystko", - helpText: - "<p>Możesz skopiować wartości z poszczególnych wierszy<br>" + - "do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" + - "<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" + - "Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>", - }; - case "no": - return { - theListContainsNoEntries: "Listen inneholder ingen oppføringer", - copyAll: "Kopier alt", - helpText: - "<p>Du kan kopiere verdier fra enkeltrader<br>" + - "til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" + - "<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" + - "Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>", - }; - case "dk": - return { - theListContainsNoEntries: "Listen indeholder ingen poster", - copyAll: "Kopiér alt", - helpText: - "<p>Du kan kopiere værdier fra enkelte rækker<br>" + - "til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" + - "<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" + - "Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>", - }; - case "sw": - return { - theListContainsNoEntries: "Listan innehåller inga poster", - copyAll: "Kopiera allt", - helpText: - "<p>Du kan kopiera värden från enskilda rader<br>" + - "till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" + - "<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" + - "Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>", - }; - - case "en": - default: - return { - theListContainsNoEntries: "The list contains no entries", - copyAll: "Copy all", - helpText: - "<p>You can copy the values from individual rows<br>" + - "to the clipboard by double-clicking on the relevant column.</p>" + - "<p>To copy an entire row, hold down the Shift key while clicking.<br>" + - "If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>", - }; - } + const locale = getLocaleOfDocument(); + switch (locale.language) { + case "de": + return { + theListContainsNoEntries: "Die Liste enthält keine Einträge", + copyAll: "Alles kopieren", + helpText: + "<p>Sie können die Werte aus einzelnen Zeilen<br>" + + "in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" + + "<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" + + "Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>", + }; + case "fr": + return { + theListContainsNoEntries: "La liste ne contient aucune entrée", + copyAll: "Copier tout", + helpText: + "<p>Vous pouvez copier les valeurs des rangées individuelles<br>" + + "dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" + + "<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" + + "Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>", + }; + case "sp": + return { + theListContainsNoEntries: "La lista no contiene entradas", + copyAll: "Copiar todo", + helpText: + "<p>Puedes copiar los valores de filas individuales<br>" + + "al portapapeles haciendo doble clic en la columna correspondiente.</p>" + + "<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" + + "Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>", + }; + case "it": + return { + theListContainsNoEntries: "L'elenco non contiene voci", + copyAll: "Copia tutto", + helpText: + "<p>Puoi copiare i valori dalle singole righe<br>" + + "negli appunti facendo doppio clic sulla colonna relativa.</p>" + + "<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" + + "Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>", + }; + case "pl": + return { + theListContainsNoEntries: "Lista nie zawiera wpisów", + copyAll: "Kopiuj wszystko", + helpText: + "<p>Możesz skopiować wartości z poszczególnych wierszy<br>" + + "do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" + + "<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" + + "Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>", + }; + case "no": + return { + theListContainsNoEntries: "Listen inneholder ingen oppføringer", + copyAll: "Kopier alt", + helpText: + "<p>Du kan kopiere verdier fra enkeltrader<br>" + + "til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" + + "<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" + + "Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>", + }; + case "dk": + return { + theListContainsNoEntries: "Listen indeholder ingen poster", + copyAll: "Kopiér alt", + helpText: + "<p>Du kan kopiere værdier fra enkelte rækker<br>" + + "til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" + + "<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" + + "Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>", + }; + case "sw": + return { + theListContainsNoEntries: "Listan innehåller inga poster", + copyAll: "Kopiera allt", + helpText: + "<p>Du kan kopiera värden från enskilda rader<br>" + + "till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" + + "<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" + + "Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>", + }; + + case "en": + default: + return { + theListContainsNoEntries: "The list contains no entries", + copyAll: "Copy all", + helpText: + "<p>You can copy the values from individual rows<br>" + + "to the clipboard by double-clicking on the relevant column.</p>" + + "<p>To copy an entire row, hold down the Shift key while clicking.<br>" + + "If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>", + }; + } } /** @@ -1228,79 +1213,80 @@ function getTranslations() { * @return {string} */ export function getStoredOrderConfigKey() { - return generateUniqueConfigKey("datatable", this?.id, "stored-order"); + return generateUniqueConfigKey("datatable", this?.id, "stored-order"); } /** * @private */ function storeOrderStatement(doFetch) { - const headers = this.getOption("headers"); - const statement = createOrderStatement(headers); - setDataSource.call(this, {orderBy: statement}, doFetch); + const headers = this.getOption("headers"); + const delimiter = this.getOption("datasource.orderDelimiter"); + const statement = createOrderStatement(headers, delimiter); + setDataSource.call(this, { order: statement }, doFetch); - const host = findElementWithSelectorUpwards(this, "monster-host"); - if (!(host && this.id)) { - return; - } + const host = findElementWithSelectorUpwards(this, "monster-host"); + if (!(host && this.id)) { + return; + } - const configKey = getStoredOrderConfigKey.call(this); + const configKey = getStoredOrderConfigKey.call(this); - // statement explode with , and remove all empty - const list = statement.split(",").filter((item) => item.trim() !== ""); - if (list.length === 0) { - return; - } + // statement explode with , and remove all empty + const list = statement.split(",").filter((item) => item.trim() !== ""); + if (list.length === 0) { + return; + } - host.setConfig(configKey, list); + host.setConfig(configKey, list); } /** * @private */ function updateGrid() { - if (!this[gridElementSymbol]) { - throw new Error("no grid element is defined"); - } - - let gridTemplateColumns = ""; - - const headers = this.getOption("headers"); - - let styles = ""; - - for (let i = 0; i < headers.length; i++) { - const header = headers[i]; - - if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { - styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`; - styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`; - } else { - gridTemplateColumns += `${header.grid} `; - } - } - - const sheet = new CSSStyleSheet(); - if (styles !== "") sheet.replaceSync(styles); - this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet]; - - const bodyWidth = this.parentNode.clientWidth; - - const breakpoint = this.getOption("responsive.breakpoint"); - this[dataControlElementSymbol].classList.toggle( - "small", - bodyWidth <= breakpoint, - ); - - if (bodyWidth > breakpoint) { - this[gridElementSymbol].style.gridTemplateColumns = - `${gridTemplateColumns}`; - this[gridHeadersElementSymbol].style.gridTemplateColumns = - `${gridTemplateColumns}`; - } else { - this[gridElementSymbol].style.gridTemplateColumns = "auto"; - this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto"; - } + if (!this[gridElementSymbol]) { + throw new Error("no grid element is defined"); + } + + let gridTemplateColumns = ""; + + const headers = this.getOption("headers"); + + let styles = ""; + + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + + if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) { + styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`; + styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`; + } else { + gridTemplateColumns += `${header.grid} `; + } + } + + const sheet = new CSSStyleSheet(); + if (styles !== "") sheet.replaceSync(styles); + this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet]; + + const bodyWidth = this.parentNode.clientWidth; + + const breakpoint = this.getOption("responsive.breakpoint"); + this[dataControlElementSymbol].classList.toggle( + "small", + bodyWidth <= breakpoint, + ); + + if (bodyWidth > breakpoint) { + this[gridElementSymbol].style.gridTemplateColumns = + `${gridTemplateColumns}`; + this[gridHeadersElementSymbol].style.gridTemplateColumns = + `${gridTemplateColumns}`; + } else { + this[gridElementSymbol].style.gridTemplateColumns = "auto"; + this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto"; + } } /** @@ -1308,20 +1294,20 @@ function updateGrid() { * @param {Header[]} headers * @param {bool} doFetch */ -function setDataSource({orderBy}, doFetch) { - const datasource = this[datasourceLinkedElementSymbol]; +function setDataSource({ order }, doFetch) { + const datasource = this[datasourceLinkedElementSymbol]; - if (!datasource) { - return; - } + if (!datasource) { + return; + } - if (isFunction(datasource?.setParameters)) { - datasource.setParameters({orderBy}); - } + if (isFunction(datasource?.setParameters)) { + datasource.setParameters({ order }); + } - if (doFetch !== false && isFunction(datasource?.fetch)) { - datasource.fetch(); - } + if (doFetch !== false && isFunction(datasource?.fetch)) { + datasource.fetch(); + } } /** @@ -1329,30 +1315,30 @@ function setDataSource({orderBy}, doFetch) { * @return {DataTable} */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } - this[dataControlElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=control]", - ); + this[dataControlElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=control]", + ); - this[gridElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=datatable]", - ); + this[gridElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=datatable]", + ); - this[gridHeadersElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=datatable-headers]", - ); + this[gridHeadersElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=datatable-headers]", + ); - this[columnBarElementSymbol] = - this.shadowRoot.querySelector("monster-column-bar"); + this[columnBarElementSymbol] = + this.shadowRoot.querySelector("monster-column-bar"); - this[copyAllElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=copy-all]", - ); + this[copyAllElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=copy-all]", + ); - return this; + return this; } /** @@ -1362,22 +1348,25 @@ function initControlReferences() { * @throws {Error} the datasource could not be initialized */ function initOptionsFromArguments() { - const options = {}; - const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); + const options = {}; + const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); if (selector) { options.datasource = {selector: selector}; } - const breakpoint = this.getAttribute( - ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, - ); - if (breakpoint) { - options.responsive = {}; - options.responsive.breakpoint = parseInt(breakpoint); - } + options.datasource.orderDelimiter = "," // workaround for the missing orderDelimiter + + const breakpoint = this.getAttribute( + ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT, + ); + + if (breakpoint) { + options.responsive = {}; + options.responsive.breakpoint = parseInt(breakpoint); + } - return options; + return options; } /** @@ -1385,7 +1374,7 @@ function initOptionsFromArguments() { * @return {string} */ function getEmptyTemplate() { - return `<monster-state data-monster-role="empty-without-action"> + return `<monster-state data-monster-role="empty-without-action"> <div part="visual"> <svg width="4rem" height="4rem" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/> @@ -1403,8 +1392,8 @@ function getEmptyTemplate() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control"> <template id="headers-row"> <div data-monster-attributes="class path:headers-row.classes, diff --git a/source/components/datatable/datatable/header.mjs b/source/components/datatable/datatable/header.mjs index d024151e261162b374ea8bb53633ef6775039be4..554242d023b614e8d0748b165ae597b66fa071f2 100644 --- a/source/components/datatable/datatable/header.mjs +++ b/source/components/datatable/datatable/header.mjs @@ -21,6 +21,7 @@ import { validateIterable, validateInstance, } from "../../../types/validate.mjs"; +import {Formatter} from "../../../text/formatter.mjs"; export { Header, @@ -104,6 +105,7 @@ class Header extends Base { mode: undefined, grid: undefined, features: undefined, + orderTemplate: undefined, }; } @@ -238,10 +240,11 @@ class Header extends Base { /** * @private - * @param {Array<Header>} headers + * @param {Header[]} headers + * @param {string} delimiter * @return {string} */ -function createOrderStatement(headers) { +function createOrderStatement(headers, delimiter = ",") { validateIterable(headers); const oderStatement = []; @@ -254,7 +257,7 @@ function createOrderStatement(headers) { } oderStatement.push(order); } - return oderStatement.join(","); + return oderStatement.join(delimiter); } /** @@ -274,7 +277,13 @@ function updateStruct() { } if (direction) { - order = `${field} ${direction}`.trim(); + const tmpl = this.getInternal("orderTemplate"); + const formatter = new Formatter({ + "direction": direction, + "label": label, + "field": field, + }); + order = formatter.format(tmpl); } this.setInternal("order", order); diff --git a/source/components/form/action-button.mjs b/source/components/form/action-button.mjs index e5b567a728a56984c6828393b4c09dc1add110a3..022fc57b4f6dba19f72e8540a694abbc2343732d 100644 --- a/source/components/form/action-button.mjs +++ b/source/components/form/action-button.mjs @@ -31,7 +31,7 @@ import { ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { ActionButton }; diff --git a/source/components/form/api-button.mjs b/source/components/form/api-button.mjs index 8b7183e1292f35cdb7ae031cfe651d6e872c1805..2c1513d75e2d46af1c032771466df62bba236ec9 100644 --- a/source/components/form/api-button.mjs +++ b/source/components/form/api-button.mjs @@ -35,7 +35,7 @@ import { ApiButtonStyleSheet } from "./stylesheet/api-button.mjs"; import { isObject, isFunction } from "../../types/is.mjs"; import { getGlobal } from "../../types/global.mjs"; import { Formatter } from "../../text/formatter.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { ApiButton }; /** diff --git a/source/components/form/button-bar.mjs b/source/components/form/button-bar.mjs index 76db511bd406b74bed6828813a80adddc5b7cbf5..1ff6671bd0e44fdaaa360a54573d38af20df8548 100644 --- a/source/components/form/button-bar.mjs +++ b/source/components/form/button-bar.mjs @@ -41,7 +41,7 @@ import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs"; import { ButtonBarStyleSheet } from "./stylesheet/button-bar.mjs"; import { positionPopper } from "./util/floating-ui.mjs"; import { convertToPixels } from "../../dom/dimension.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { ButtonBar }; /** diff --git a/source/components/form/button.mjs b/source/components/form/button.mjs index f99c16c0299de90b7bc26856d7cc615ed72ed5b9..0df4d5946dbc14a043b729fb7b5114ac00b82950 100644 --- a/source/components/form/button.mjs +++ b/source/components/form/button.mjs @@ -30,7 +30,7 @@ import { ATTRIBUTE_BUTTON_CLASS } from "./constants.mjs"; import { ButtonStyleSheet } from "./stylesheet/button.mjs"; import { RippleStyleSheet } from "../stylesheet/ripple.mjs"; import { fireCustomEvent } from "../../dom/events.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { Button }; diff --git a/source/components/form/context-error.mjs b/source/components/form/context-error.mjs index 1885544e54eea7b71d64061a0a7d40faf6f4fca3..0b35504599d498d3fa17bce801e99ee4e19ad0ae 100644 --- a/source/components/form/context-error.mjs +++ b/source/components/form/context-error.mjs @@ -24,7 +24,7 @@ import { ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { ContextError }; diff --git a/source/components/form/field-set.mjs b/source/components/form/field-set.mjs index b689981e27ba653ab253d3fbfa906e00cba358eb..11a2496603d4a738ac415e2fc977063cca82590a 100644 --- a/source/components/form/field-set.mjs +++ b/source/components/form/field-set.mjs @@ -29,7 +29,7 @@ import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs"; import "../layout/collapse.mjs"; import "./toggle-switch.mjs"; import { getLocaleOfDocument } from "../../dom/locale.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { FieldSet }; diff --git a/source/components/form/popper-button.mjs b/source/components/form/popper-button.mjs index a6c1b25e7b85614832e08644f1418c1d08c585de..1b46cf718f997013ab94b42acf1dabe9f6d5cd59 100644 --- a/source/components/form/popper-button.mjs +++ b/source/components/form/popper-button.mjs @@ -12,27 +12,27 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {instanceSymbol} from "../../constants.mjs"; -import {addAttributeToken} from "../../dom/attributes.mjs"; +import { instanceSymbol } from "../../constants.mjs"; +import { addAttributeToken } from "../../dom/attributes.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_ROLE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { - assembleMethodSymbol, - registerCustomElement, + assembleMethodSymbol, + registerCustomElement, } from "../../dom/customelement.mjs"; -import {getDocument} from "../../dom/util.mjs"; -import {isFunction} from "../../types/is.mjs"; -import {DeadMansSwitch} from "../../util/deadmansswitch.mjs"; -import {Popper} from "./popper.mjs"; -import {STYLE_DISPLAY_MODE_BLOCK} from "./constants.mjs"; -import {PopperButtonStyleSheet} from "./stylesheet/popper-button.mjs"; -import {positionPopper} from "./util/floating-ui.mjs"; +import { getDocument } from "../../dom/util.mjs"; +import { isFunction } from "../../types/is.mjs"; +import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; +import { Popper } from "./popper.mjs"; +import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs"; +import { PopperButtonStyleSheet } from "./stylesheet/popper-button.mjs"; +import { positionPopper } from "./util/floating-ui.mjs"; import "./button.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; -export {PopperButton}; +export { PopperButton }; /** * @private @@ -141,205 +141,205 @@ const arrowElementSymbol = Symbol("arrowElement"); * @fires monster-changed */ class PopperButton extends Popper { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for( - "@schukai/monster/components/form/popper-button@@instance", - ); - } - - /** - * 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} templates - The templates for the control. - * @property {string} templates.main - The main template. - * @property {object} labels - The labels for the control. - * @property {string} labels.button - The label for the button. - * @property {string} content - The content of the popper. - * @property {object} popper - The popper options. - * @extends {Button.defaults} - */ - get defaults() { - return Object.assign({}, super.defaults, { - templates: { - main: getTemplate(), - }, - actions: { - click: (e) => { - this.toggleDialog(); - }, - }, - classes: { - button: "monster-button", - }, - labels: { - button: '<slot name="button"></slot>', - }, - mode: "click", - value: null, - }); - } - - /** - * - * @return {Monster.Components.Form.PopperButton} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - initControlReferences.call(this); - initEventHandler.call(this); - - return this; - } - - /** - * @return {string} - */ - static getTag() { - return "monster-popper-button"; - } - - /** - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - const styles = super.getCSSStyleSheet(); - styles.push(PopperButtonStyleSheet); - return styles; - } - - /** - * @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]); - } - - updatePopper.call(this); - attachResizeObserver.call(this); - } - - /** - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - - // close on outside ui-events - for (const [, type] of Object.entries(["click", "touch"])) { - document.removeEventListener(type, this[closeEventHandler]); - } - - disconnectResizeObserver.call(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; - } - - if ( - this[buttonElementSymbol] && - isFunction(this[buttonElementSymbol].click) - ) { - this[buttonElementSymbol].click(); - } - } - - /** - * The Button.focus() method sets focus on the internal button element. - * - * @since 3.27.0 - * @param {Object} options - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} - */ - focus(options) { - if (this.getOption("disabled") === true) { - return; - } - - if ( - this[buttonElementSymbol] && - isFunction(this[buttonElementSymbol].focus) - ) { - this[buttonElementSymbol].focus(options); - } - } - - /** - * The Button.blur() method removes focus from the internal button element. - */ - blur() { - if ( - this[buttonElementSymbol] && - isFunction(this[buttonElementSymbol].blur) - ) { - this[buttonElementSymbol].blur(); - } - } - - /** - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} - * @return {boolean} - */ - static get formAssociated() { - return true; - } - - /** - * The current selection of the Select - * - * ``` - * e = document.querySelector('monster-select'); - * console.log(e.value) - * // ↦ 1 - * // ↦ ['1','2'] - * ``` - * - * @property {string|array} - */ - get value() { - return this.getOption("value"); - } - - /** - * Set selection - * - * ``` - * e = document.querySelector('monster-select'); - * e.value=1 - * ``` - * - * @property {string|array} value - * @throws {Error} unsupported type - */ - set value(value) { - this.setOption("value", value); - try { - this?.setFormValue(this.value); - } catch (e) { - addErrorAttribute(this, e); - } - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for( + "@schukai/monster/components/form/popper-button@@instance", + ); + } + + /** + * 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} templates - The templates for the control. + * @property {string} templates.main - The main template. + * @property {object} labels - The labels for the control. + * @property {string} labels.button - The label for the button. + * @property {string} content - The content of the popper. + * @property {object} popper - The popper options. + * @extends {Button.defaults} + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + actions: { + click: (e) => { + this.toggleDialog(); + }, + }, + classes: { + button: "monster-button", + }, + labels: { + button: '<slot name="button"></slot>', + }, + mode: "click", + value: null, + }); + } + + /** + * + * @return {Monster.Components.Form.PopperButton} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + + return this; + } + + /** + * @return {string} + */ + static getTag() { + return "monster-popper-button"; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + const styles = super.getCSSStyleSheet(); + styles.push(PopperButtonStyleSheet); + return styles; + } + + /** + * @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]); + } + + updatePopper.call(this); + attachResizeObserver.call(this); + } + + /** + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + + // close on outside ui-events + for (const [, type] of Object.entries(["click", "touch"])) { + document.removeEventListener(type, this[closeEventHandler]); + } + + disconnectResizeObserver.call(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; + } + + if ( + this[buttonElementSymbol] && + isFunction(this[buttonElementSymbol].click) + ) { + this[buttonElementSymbol].click(); + } + } + + /** + * The Button.focus() method sets focus on the internal button element. + * + * @since 3.27.0 + * @param {Object} options + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} + */ + focus(options) { + if (this.getOption("disabled") === true) { + return; + } + + if ( + this[buttonElementSymbol] && + isFunction(this[buttonElementSymbol].focus) + ) { + this[buttonElementSymbol].focus(options); + } + } + + /** + * The Button.blur() method removes focus from the internal button element. + */ + blur() { + if ( + this[buttonElementSymbol] && + isFunction(this[buttonElementSymbol].blur) + ) { + this[buttonElementSymbol].blur(); + } + } + + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @return {boolean} + */ + static get formAssociated() { + return true; + } + + /** + * The current selection of the Select + * + * ``` + * e = document.querySelector('monster-select'); + * console.log(e.value) + * // ↦ 1 + * // ↦ ['1','2'] + * ``` + * + * @property {string|array} + */ + get value() { + return this.getOption("value"); + } + + /** + * Set selection + * + * ``` + * e = document.querySelector('monster-select'); + * e.value=1 + * ``` + * + * @property {string|array} value + * @throws {Error} unsupported type + */ + set value(value) { + this.setOption("value", value); + try { + this?.setFormValue(this.value); + } catch (e) { + addErrorAttribute(this, e); + } + } } /** @@ -347,67 +347,67 @@ class PopperButton extends Popper { * @return {initEventHandler} */ function initEventHandler() { - this[closeEventHandler] = (event) => { - const path = event.composedPath(); - - for (const [, element] of Object.entries(path)) { - if (element === this) { - return; - } - } - this.hideDialog(); - }; - - return this; + this[closeEventHandler] = (event) => { + const path = event.composedPath(); + + for (const [, element] of Object.entries(path)) { + if (element === this) { + return; + } + } + this.hideDialog(); + }; + + return this; } /** * @private */ function attachResizeObserver() { - // against flickering - this[resizeObserverSymbol] = new ResizeObserver(() => { - if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { - try { - this[timerCallbackSymbol].touch(); - return; - } catch (e) { - delete this[timerCallbackSymbol]; - } - } - - this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { - updatePopper.call(this); - }); - }); - - this[resizeObserverSymbol].observe(this.parentElement); + // against flickering + this[resizeObserverSymbol] = new ResizeObserver(() => { + if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[timerCallbackSymbol].touch(); + return; + } catch (e) { + delete this[timerCallbackSymbol]; + } + } + + this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { + updatePopper.call(this); + }); + }); + + this[resizeObserverSymbol].observe(this.parentElement); } function disconnectResizeObserver() { - if (this[resizeObserverSymbol] instanceof ResizeObserver) { - this[resizeObserverSymbol].disconnect(); - } + if (this[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } } /** * @private */ function updatePopper() { - if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { - return; - } - - if (this.getOption("disabled", false) === true) { - return; - } - - positionPopper.call( - this, - this[controlElementSymbol], - this[popperElementSymbol], - this.getOption("popper", {}), - ); + if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { + return; + } + + if (this.getOption("disabled", false) === true) { + return; + } + + positionPopper.call( + this, + this[controlElementSymbol], + this[popperElementSymbol], + this.getOption("popper", {}), + ); } /** @@ -415,21 +415,21 @@ function updatePopper() { * @return {Select} */ function initControlReferences() { - this[controlElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=control]`, - ); + this[controlElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=control]`, + ); - this[buttonElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=button]`, - ); + this[buttonElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=button]`, + ); - this[popperElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=popper]`, - ); + this[popperElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=popper]`, + ); - this[arrowElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=arrow]`, - ); + this[arrowElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=arrow]`, + ); } /** @@ -437,8 +437,8 @@ function initControlReferences() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div data-monster-role="control" part="control"> <button data-monster-attributes="disabled path:disabled | if:true, class path:classes.button" data-monster-role="button" diff --git a/source/components/form/reload.mjs b/source/components/form/reload.mjs index 2120f17382b436297e094260202a49daf912ac4d..639384c7be6754d3a7f0bb5b434e26ea64cb863c 100644 --- a/source/components/form/reload.mjs +++ b/source/components/form/reload.mjs @@ -28,7 +28,7 @@ import { import { isString } from "../../types/is.mjs"; import { ATTRIBUTE_FORM_RELOAD, ATTRIBUTE_FORM_URL } from "./constants.mjs"; import { loadAndAssignContent } from "./util/fetch.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { Reload }; diff --git a/source/components/form/toggle-switch.mjs b/source/components/form/toggle-switch.mjs index 8cd47552b26d2b9c54fdca917b5885d48b367300..df80f449d970cfb85855da632d6db497de760408 100644 --- a/source/components/form/toggle-switch.mjs +++ b/source/components/form/toggle-switch.mjs @@ -31,7 +31,7 @@ import { } from "../../dom/constants.mjs"; import { getWindow } from "../../dom/util.mjs"; import { fireEvent } from "../../dom/events.mjs"; -import {addErrorAttribute} from "../../dom/error.mjs"; +import { addErrorAttribute } from "../../dom/error.mjs"; export { ToggleSwitch }; diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs index 6ed82c75349409eaf878394534715b3980b5b0a9..6852f1dd0e06d7a11e5863dab54ce75a910a5081 100644 --- a/source/dom/customelement.mjs +++ b/source/dom/customelement.mjs @@ -12,59 +12,56 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {findElementWithIdUpwards, getDocument, getWindow} 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, getDocument, getWindow } 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 { getLinkedObjects, hasObjectLink } from "./attributes.mjs"; import { - getLinkedObjects, - hasObjectLink, -} from "./attributes.mjs"; -import { - ATTRIBUTE_DISABLED, - ATTRIBUTE_OPTIONS, - ATTRIBUTE_INIT_CALLBACK, - ATTRIBUTE_OPTIONS_SELECTOR, - ATTRIBUTE_SCRIPT_HOST, - customElementUpdaterLinkSymbol, - initControlCallbackName, + ATTRIBUTE_DISABLED, + 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 {addErrorAttribute} from "./error.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 { addErrorAttribute } from "./error.mjs"; export { - CustomElement, - initMethodSymbol, - assembleMethodSymbol, - attributeObserverSymbol, - registerCustomElement, - getSlottedElements, - updaterTransformerMethodsSymbol, + CustomElement, + initMethodSymbol, + assembleMethodSymbol, + attributeObserverSymbol, + registerCustomElement, + getSlottedElements, + updaterTransformerMethodsSymbol, }; /** @@ -76,14 +73,14 @@ const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol"); * @type {symbol} */ const assembleMethodSymbol = Symbol.for( - "@schukai/monster/dom/@@assembleMethodSymbol", + "@schukai/monster/dom/@@assembleMethodSymbol", ); /** * @type {symbol} */ const updaterTransformerMethodsSymbol = Symbol.for( - "@schukai/monster/dom/@@updaterTransformerMethodsSymbol", + "@schukai/monster/dom/@@updaterTransformerMethodsSymbol", ); /** @@ -91,7 +88,7 @@ const updaterTransformerMethodsSymbol = Symbol.for( * @type {symbol} */ const attributeObserverSymbol = Symbol.for( - "@schukai/monster/dom/@@attributeObserver", + "@schukai/monster/dom/@@attributeObserver", ); /** @@ -99,7 +96,7 @@ const attributeObserverSymbol = Symbol.for( * @type {symbol} */ const attributeMutationObserverSymbol = Symbol( - "@schukai/monster/dom/@@mutationObserver", + "@schukai/monster/dom/@@mutationObserver", ); /** @@ -188,558 +185,555 @@ 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 option attribute does not contain a valid JSON definition. - */ - constructor() { - super(); - - this[attributeObserverSymbol] = {}; - this[internalSymbol] = new ProxyObserver({ - options: initOptionsFromAttributes(this, extend({}, this.defaults)), - }); - this[initMethodSymbol](); - initOptionObserver.call(this); - this[scriptHostElementSymbol] = []; - } - - /** - * This method is called by the `instanceof` operator. - * - * @return {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/dom/custom-element@@instance"); - } - - /** - * This method determines which attributes are to be - * monitored by `attributeChangedCallback()`. Unfortunately, this method is static. - * Therefore, the `observedAttributes` property cannot be changed during runtime. - * - * @return {string[]} - * @since 1.15.0 - */ - static get observedAttributes() { - return []; - } - - /** - * - * @param attribute - * @param callback - * @return {CustomElement} - */ - addAttributeObserver(attribute, callback) { - validateFunction(callback); - this[attributeObserverSymbol][attribute] = callback; - return this; - } - - /** - * - * @param attribute - * @return {CustomElement} - */ - removeAttributeObserver(attribute) { - delete this[attributeObserverSymbol][attribute]; - return this; - } - - /** - * The `defaults` property defines the default values for a control. If you want to override these, - * you can use various methods, which are described in the documentation available at - * {@link https://monsterjs.orgendocconfigurate-a-monster-control}. - * - * The individual configuration values are listed below: - * - * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow), - * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements). - * - * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template). - * - * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot). - * - * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form. - * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it. - * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling. - * @property {Object} templates Specifies the templates used by the control. - * @property {string} templates.main=undefined Specifies the main template used by the control. - * @property {Object} templateMapping Specifies the mapping of templates. - * @property {Object} templateFormatter Specifies the formatter for the templates. - * @property {Object} templateFormatter.marker Specifies the marker for the templates. - * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates. - * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates. - * @property {Boolean} eventProcessing=false Specifies whether the control processes events. - * @since 1.8.0 - */ - get defaults() { - return { - disabled: false, - shadowMode: "open", - delegatesFocus: true, - templates: { - main: undefined, - }, - templateMapping: {}, - templateFormatter: { - marker: { - open: null, - close: null, - }, - }, - - eventProcessing: false, - }; - } - - /** - * This method updates the labels of the element. - * The labels are defined in the option object. - * The key of the label is used to retrieve the translation from the document. - * If the translation is different from the label, the label is updated. - * - * Before you can use this method, you must have loaded the translations. - * - * @return {CustomElement} - * @throws {Error} Cannot find an element with translations. Add a translation object to the document. - */ - updateI18n() { - let translations; - - try { - translations = getDocumentTranslations(); - } catch (e) { - addErrorAttribute(this, e); - return this; - } - - if (!translations) { - return this; - } - - const labels = this.getOption("labels"); - if (!(isObject(labels) || isIterable(labels))) { - return this; - } - - for (const key in labels) { - const def = labels[key]; - - if (isString(def)) { - const text = translations.getText(key, def); - if (text !== def) { - this.setOption(`labels.${key}`, text); - } - continue; - } else if (isObject(def)) { - for (const k in def) { - const d = def[k]; - - const text = translations.getPluralRuleText(key, k, d); - if (!isString(text)) { - throw new Error("Invalid labels definition"); - } - if (text !== d) { - this.setOption(`labels.${key}.${k}`, text); - } - } - continue; - } - - throw new Error("Invalid labels definition"); - } - return this; - } - - /** - * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten - * by the derived class. - * - * Note that there is no check on the name of the tag in this class. It is the responsibility of - * the developer to assign an appropriate tag name. If the name is not valid, the - * `registerCustomElement()` method will issue an error. - * - * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name - * @throws {Error} This method must be overridden by the derived class. - * @return {string} The tag name associated with the custom element. - * @since 1.7.0 - */ - static getTag() { - throw new Error( - "The method `getTag()` must be overridden by the derived class.", - ); - } - - /** - * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element. - * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour. - * - * If `undefined` is returned, then the shadow root does not receive a stylesheet. - * - * Example usage: - * - * ```js - * class MyElement extends CustomElement { - * static getCSSStyleSheet() { - * const sheet = new CSSStyleSheet(); - * sheet.replaceSync("p { color: red; }"); - * return sheet; - * } - * } - * ``` - * - * If the environment does not support the `CSSStyleSheet` constructor, - * you can use the following workaround to create the stylesheet: - * - * ```js - * const doc = document.implementation.createHTMLDocument('title'); - * let style = doc.createElement("style"); - * style.innerHTML = "p { color: red; }"; - * style.appendChild(document.createTextNode("")); - * doc.head.appendChild(style); - * return doc.styleSheets[0]; - * ``` - * - * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied. - */ - static getCSSStyleSheet() { - return undefined; - } - - /** - * attach a new observer - * - * @param {Observer} observer - * @return {CustomElement} - */ - attachObserver(observer) { - this[internalSymbol].attachObserver(observer); - return this; - } - - /** - * detach a observer - * - * @param {Observer} observer - * @return {CustomElement} - */ - detachObserver(observer) { - this[internalSymbol].detachObserver(observer); - return this; - } - - /** - * @param {Observer} observer - * @return {ProxyObserver} - */ - containsObserver(observer) { - return this[internalSymbol].containsObserver(observer); - } - - /** - * nested options can be specified by path `a.b.c` - * - * @param {string} path - * @param {*} defaultValue - * @return {*} - * @since 1.10.0 - */ - getOption(path, defaultValue = undefined) { - let value; - - try { - value = new Pathfinder( - this[internalSymbol].getRealSubject()["options"], - ).getVia(path); - } catch (e) { - } - - if (value === undefined) return defaultValue; - return value; - } - - /** - * Set option and inform elements - * - * @param {string} path - * @param {*} value - * @return {CustomElement} - * @since 1.14.0 - */ - setOption(path, value) { - new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia( - path, - value, - ); - return this; - } - - /** - * @since 1.15.0 - * @param {string|object} options - * @return {CustomElement} - */ - setOptions(options) { - if (isString(options)) { - options = parseOptionsJSON.call(this, options); - } - // 2024-01-21: remove this.defaults, otherwise it will overwrite - // the current settings that have already been made. - // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136 - extend(this[internalSymbol].getSubject()["options"], options); - - return this; - } - - /** - * Is called once via the constructor - * - * @return {CustomElement} - * @since 1.8.0 - */ - [initMethodSymbol]() { - return this; - } - - /** - * This method is called once when the object is equipped with update for the dynamic change of the dom. - * The functions returned here can be used as pipe functions in the template. - * - * In the example, the function `my-transformer` is defined. In the template, you can use it as follows: - * - * ```html - * <my-element - * data-monster-option-transformer="path:my-value | call:my-transformer"> - * </my-element> - * ``` - * - * The function `my-transformer` is called with the value of `my-value` as a parameter. - * - * ```js - * class MyElement extends CustomElement { - * [updaterTransformerMethodsSymbol]() { - * return { - * "my-transformer": (value) => { - * switch (typeof Wert) { - * case "string": - * return value + "!"; - * case "Zahl": - * return value + 1; - * default: - * return value; - * } - * } - * }; - * }; - * } - * ``` - * - * @return {object} - * @since 2.43.0 - */ - [updaterTransformerMethodsSymbol]() { - return {}; - } - - /** - * This method is called once when the object is included in the DOM for the first time. It performs the following actions: - * - * <ol> - * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li> - * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li> - * <li>Initializes the HTML content of the element.</li> - * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li> - * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li> - * - * @return {CustomElement} - The updated custom element. - * @since 1.8.0 - */ - [assembleMethodSymbol]() { - let elements; - let nodeList; - - // Extract options from attributes and set them - const AttributeOptions = getOptionsFromAttributes.call(this); - if ( - isObject(AttributeOptions) && - Object.keys(AttributeOptions).length > 0 - ) { - this.setOptions(AttributeOptions); - } - - // Extract options from script tag and set them - const ScriptOptions = getOptionsFromScriptTag.call(this); - if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) { - this.setOptions(ScriptOptions); - } - - // Initialize the shadow root and its CSS stylesheet - if (this.getOption("shadowMode", false) !== false) { - try { - initShadowRoot.call(this); - elements = this.shadowRoot.childNodes; - } catch (e) { - addErrorAttribute(this, e); - } - - try { - initCSSStylesheet.call(this); - } catch (e) { - addErrorAttribute(this, e); - } - } - - // 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; - } - - try { - this[updateCloneDataSymbol] = clone( - this[internalSymbol].getRealSubject()["options"], - ); - } catch (e) { - addErrorAttribute(this, e); - } - - const cfg = {}; - if (this.getOption("eventProcessing") === true) { - cfg.eventProcessing = true; - } - - addObjectWithUpdaterToElement.call( - this, - nodeList, - customElementUpdaterLinkSymbol, - this[updateCloneDataSymbol], - cfg, - ); - - // Attach a mutation observer to observe changes to the attributes of the element - attachAttributeChangeMutationObserver.call(this); - - return this; - } - - /** - * You know what you are doing? This function is only for advanced users. - * The result is a clone of the internal data. - * - * @return {*} - */ - getInternalUpdateCloneData() { - return clone(this[updateCloneDataSymbol]); - } - - /** - * This method is called every time the element is inserted into the DOM. It checks if the custom element - * has already been initialized and if not, calls the assembleMethod to initialize it. - * - * @return {void} - * @since 1.7.0 - * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback - */ - connectedCallback() { - // Check if the object has already been initialized - if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) { - // If not, call the assembleMethod to initialize the object - this[assembleMethodSymbol](); - } - } - - /** - * Called every time the element is removed from the DOM. Useful for running clean up code. - * - * @return {void} - * @since 1.7.0 - */ - disconnectedCallback() { - } - - /** - * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)). - * - * @return {void} - * @since 1.7.0 - */ - adoptedCallback() { - } - - /** - * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial - * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes - * property will receive this callback. - * - * @param {string} attrName - * @param {string} oldVal - * @param {string} newVal - * @return {void} - * @since 1.15.0 - */ - attributeChangedCallback(attrName, oldVal, newVal) { - if (attrName.startsWith("data-monster-option-")) { - setOptionFromAttribute( - this, - attrName, - this[internalSymbol].getSubject()["options"], - ); - } - - const callback = this[attributeObserverSymbol]?.[attrName]; - if (isFunction(callback)) { - try { - callback.call(this, newVal, oldVal); - } catch (e) { - addErrorAttribute(this, e); - } - } - } - - /** - * - * @param {Node} node - * @return {boolean} - * @throws {TypeError} value is not an instance of - * @since 1.19.0 - */ - hasNode(node) { - if (containChildNode.call(this, validateInstance(node, Node))) { - return true; - } - - if (!(this.shadowRoot instanceof ShadowRoot)) { - return false; - } - - return containChildNode.call(this.shadowRoot, node); - } - - /** - * Calls a callback function if it exists. - * - * @param {string} name - * @param {*} args - * @return {*} - */ - callCallback(name, args) { - return callControlCallback.call(this, name, ...args); - } + /** + * A new object is created. First, the `initOptions` method is called. Here the + * options can be defined in derived classes. Subsequently, the shadowRoot is initialized. + * + * IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via <code>document.createElement()</code>. + * + * @throws {Error} the option attribute does not contain a valid JSON definition. + */ + constructor() { + super(); + + this[attributeObserverSymbol] = {}; + this[internalSymbol] = new ProxyObserver({ + options: initOptionsFromAttributes(this, extend({}, this.defaults)), + }); + this[initMethodSymbol](); + initOptionObserver.call(this); + this[scriptHostElementSymbol] = []; + } + + /** + * This method is called by the `instanceof` operator. + * + * @return {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/dom/custom-element@@instance"); + } + + /** + * This method determines which attributes are to be + * monitored by `attributeChangedCallback()`. Unfortunately, this method is static. + * Therefore, the `observedAttributes` property cannot be changed during runtime. + * + * @return {string[]} + * @since 1.15.0 + */ + static get observedAttributes() { + return []; + } + + /** + * + * @param attribute + * @param callback + * @return {CustomElement} + */ + addAttributeObserver(attribute, callback) { + validateFunction(callback); + this[attributeObserverSymbol][attribute] = callback; + return this; + } + + /** + * + * @param attribute + * @return {CustomElement} + */ + removeAttributeObserver(attribute) { + delete this[attributeObserverSymbol][attribute]; + return this; + } + + /** + * The `defaults` property defines the default values for a control. If you want to override these, + * you can use various methods, which are described in the documentation available at + * {@link https://monsterjs.orgendocconfigurate-a-monster-control}. + * + * The individual configuration values are listed below: + * + * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow), + * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements). + * + * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template). + * + * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot). + * + * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form. + * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it. + * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling. + * @property {Object} templates Specifies the templates used by the control. + * @property {string} templates.main=undefined Specifies the main template used by the control. + * @property {Object} templateMapping Specifies the mapping of templates. + * @property {Object} templateFormatter Specifies the formatter for the templates. + * @property {Object} templateFormatter.marker Specifies the marker for the templates. + * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates. + * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates. + * @property {Boolean} eventProcessing=false Specifies whether the control processes events. + * @since 1.8.0 + */ + get defaults() { + return { + disabled: false, + shadowMode: "open", + delegatesFocus: true, + templates: { + main: undefined, + }, + templateMapping: {}, + templateFormatter: { + marker: { + open: null, + close: null, + }, + }, + + eventProcessing: false, + }; + } + + /** + * This method updates the labels of the element. + * The labels are defined in the option object. + * The key of the label is used to retrieve the translation from the document. + * If the translation is different from the label, the label is updated. + * + * Before you can use this method, you must have loaded the translations. + * + * @return {CustomElement} + * @throws {Error} Cannot find an element with translations. Add a translation object to the document. + */ + updateI18n() { + let translations; + + try { + translations = getDocumentTranslations(); + } catch (e) { + addErrorAttribute(this, e); + return this; + } + + if (!translations) { + return this; + } + + const labels = this.getOption("labels"); + if (!(isObject(labels) || isIterable(labels))) { + return this; + } + + for (const key in labels) { + const def = labels[key]; + + if (isString(def)) { + const text = translations.getText(key, def); + if (text !== def) { + this.setOption(`labels.${key}`, text); + } + continue; + } else if (isObject(def)) { + for (const k in def) { + const d = def[k]; + + const text = translations.getPluralRuleText(key, k, d); + if (!isString(text)) { + throw new Error("Invalid labels definition"); + } + if (text !== d) { + this.setOption(`labels.${key}.${k}`, text); + } + } + continue; + } + + throw new Error("Invalid labels definition"); + } + return this; + } + + /** + * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten + * by the derived class. + * + * Note that there is no check on the name of the tag in this class. It is the responsibility of + * the developer to assign an appropriate tag name. If the name is not valid, the + * `registerCustomElement()` method will issue an error. + * + * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + * @throws {Error} This method must be overridden by the derived class. + * @return {string} The tag name associated with the custom element. + * @since 1.7.0 + */ + static getTag() { + throw new Error( + "The method `getTag()` must be overridden by the derived class.", + ); + } + + /** + * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element. + * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour. + * + * If `undefined` is returned, then the shadow root does not receive a stylesheet. + * + * Example usage: + * + * ```js + * class MyElement extends CustomElement { + * static getCSSStyleSheet() { + * const sheet = new CSSStyleSheet(); + * sheet.replaceSync("p { color: red; }"); + * return sheet; + * } + * } + * ``` + * + * If the environment does not support the `CSSStyleSheet` constructor, + * you can use the following workaround to create the stylesheet: + * + * ```js + * const doc = document.implementation.createHTMLDocument('title'); + * let style = doc.createElement("style"); + * style.innerHTML = "p { color: red; }"; + * style.appendChild(document.createTextNode("")); + * doc.head.appendChild(style); + * return doc.styleSheets[0]; + * ``` + * + * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied. + */ + static getCSSStyleSheet() { + return undefined; + } + + /** + * attach a new observer + * + * @param {Observer} observer + * @return {CustomElement} + */ + attachObserver(observer) { + this[internalSymbol].attachObserver(observer); + return this; + } + + /** + * detach a observer + * + * @param {Observer} observer + * @return {CustomElement} + */ + detachObserver(observer) { + this[internalSymbol].detachObserver(observer); + return this; + } + + /** + * @param {Observer} observer + * @return {ProxyObserver} + */ + containsObserver(observer) { + return this[internalSymbol].containsObserver(observer); + } + + /** + * nested options can be specified by path `a.b.c` + * + * @param {string} path + * @param {*} defaultValue + * @return {*} + * @since 1.10.0 + */ + getOption(path, defaultValue = undefined) { + let value; + + try { + value = new Pathfinder( + this[internalSymbol].getRealSubject()["options"], + ).getVia(path); + } catch (e) {} + + if (value === undefined) return defaultValue; + return value; + } + + /** + * Set option and inform elements + * + * @param {string} path + * @param {*} value + * @return {CustomElement} + * @since 1.14.0 + */ + setOption(path, value) { + new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia( + path, + value, + ); + return this; + } + + /** + * @since 1.15.0 + * @param {string|object} options + * @return {CustomElement} + */ + setOptions(options) { + if (isString(options)) { + options = parseOptionsJSON.call(this, options); + } + // 2024-01-21: remove this.defaults, otherwise it will overwrite + // the current settings that have already been made. + // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136 + extend(this[internalSymbol].getSubject()["options"], options); + + return this; + } + + /** + * Is called once via the constructor + * + * @return {CustomElement} + * @since 1.8.0 + */ + [initMethodSymbol]() { + return this; + } + + /** + * This method is called once when the object is equipped with update for the dynamic change of the dom. + * The functions returned here can be used as pipe functions in the template. + * + * In the example, the function `my-transformer` is defined. In the template, you can use it as follows: + * + * ```html + * <my-element + * data-monster-option-transformer="path:my-value | call:my-transformer"> + * </my-element> + * ``` + * + * The function `my-transformer` is called with the value of `my-value` as a parameter. + * + * ```js + * class MyElement extends CustomElement { + * [updaterTransformerMethodsSymbol]() { + * return { + * "my-transformer": (value) => { + * switch (typeof Wert) { + * case "string": + * return value + "!"; + * case "Zahl": + * return value + 1; + * default: + * return value; + * } + * } + * }; + * }; + * } + * ``` + * + * @return {object} + * @since 2.43.0 + */ + [updaterTransformerMethodsSymbol]() { + return {}; + } + + /** + * This method is called once when the object is included in the DOM for the first time. It performs the following actions: + * + * <ol> + * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li> + * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li> + * <li>Initializes the HTML content of the element.</li> + * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li> + * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li> + * + * @return {CustomElement} - The updated custom element. + * @since 1.8.0 + */ + [assembleMethodSymbol]() { + let elements; + let nodeList; + + // Extract options from attributes and set them + const AttributeOptions = getOptionsFromAttributes.call(this); + if ( + isObject(AttributeOptions) && + Object.keys(AttributeOptions).length > 0 + ) { + this.setOptions(AttributeOptions); + } + + // Extract options from script tag and set them + const ScriptOptions = getOptionsFromScriptTag.call(this); + if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) { + this.setOptions(ScriptOptions); + } + + // Initialize the shadow root and its CSS stylesheet + if (this.getOption("shadowMode", false) !== false) { + try { + initShadowRoot.call(this); + elements = this.shadowRoot.childNodes; + } catch (e) { + addErrorAttribute(this, e); + } + + try { + initCSSStylesheet.call(this); + } catch (e) { + addErrorAttribute(this, e); + } + } + + // 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; + } + + try { + this[updateCloneDataSymbol] = clone( + this[internalSymbol].getRealSubject()["options"], + ); + } catch (e) { + addErrorAttribute(this, e); + } + + const cfg = {}; + if (this.getOption("eventProcessing") === true) { + cfg.eventProcessing = true; + } + + addObjectWithUpdaterToElement.call( + this, + nodeList, + customElementUpdaterLinkSymbol, + this[updateCloneDataSymbol], + cfg, + ); + + // Attach a mutation observer to observe changes to the attributes of the element + attachAttributeChangeMutationObserver.call(this); + + return this; + } + + /** + * You know what you are doing? This function is only for advanced users. + * The result is a clone of the internal data. + * + * @return {*} + */ + getInternalUpdateCloneData() { + return clone(this[updateCloneDataSymbol]); + } + + /** + * This method is called every time the element is inserted into the DOM. It checks if the custom element + * has already been initialized and if not, calls the assembleMethod to initialize it. + * + * @return {void} + * @since 1.7.0 + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback + */ + connectedCallback() { + // Check if the object has already been initialized + if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) { + // If not, call the assembleMethod to initialize the object + this[assembleMethodSymbol](); + } + } + + /** + * Called every time the element is removed from the DOM. Useful for running clean up code. + * + * @return {void} + * @since 1.7.0 + */ + disconnectedCallback() {} + + /** + * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)). + * + * @return {void} + * @since 1.7.0 + */ + adoptedCallback() {} + + /** + * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial + * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes + * property will receive this callback. + * + * @param {string} attrName + * @param {string} oldVal + * @param {string} newVal + * @return {void} + * @since 1.15.0 + */ + attributeChangedCallback(attrName, oldVal, newVal) { + if (attrName.startsWith("data-monster-option-")) { + setOptionFromAttribute( + this, + attrName, + this[internalSymbol].getSubject()["options"], + ); + } + + const callback = this[attributeObserverSymbol]?.[attrName]; + if (isFunction(callback)) { + try { + callback.call(this, newVal, oldVal); + } catch (e) { + addErrorAttribute(this, e); + } + } + } + + /** + * + * @param {Node} node + * @return {boolean} + * @throws {TypeError} value is not an instance of + * @since 1.19.0 + */ + hasNode(node) { + if (containChildNode.call(this, validateInstance(node, Node))) { + return true; + } + + if (!(this.shadowRoot instanceof ShadowRoot)) { + return false; + } + + return containChildNode.call(this.shadowRoot, node); + } + + /** + * Calls a callback function if it exists. + * + * @param {string} name + * @param {*} args + * @return {*} + */ + callCallback(name, args) { + return callControlCallback.call(this, name, ...args); + } } /** @@ -748,49 +742,46 @@ 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) { - addErrorAttribute(this, e); - } - } - } - - addErrorAttribute( - this, - `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) { + addErrorAttribute(this, e); + } + } + } + + addErrorAttribute(this, `callback ${callBackFunctionName} not found`); } /** @@ -807,16 +798,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); } /** @@ -826,34 +817,34 @@ 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) { - addErrorAttribute(self, e); - } + 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) { + addErrorAttribute(self, e); + } } /** @@ -863,19 +854,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; } /** @@ -885,89 +876,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); + } + }; } /** @@ -976,35 +967,35 @@ 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)) { - addErrorAttribute( - this, - `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) { - addErrorAttribute( - this, - `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)) { + addErrorAttribute( + this, + `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) { + addErrorAttribute( + this, + `when analyzing the configuration from the script tag there was an error. ${e}`, + ); + } + + return obj; } /** @@ -1012,20 +1003,20 @@ function getOptionsFromScriptTag() { * @return {object} */ function getOptionsFromAttributes() { - if (this.hasAttribute(ATTRIBUTE_OPTIONS)) { - try { - return parseOptionsJSON.call(this, this.getAttribute(ATTRIBUTE_OPTIONS)); - } catch (e) { - addErrorAttribute( - this, - `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) { + addErrorAttribute( + this, + `the options attribute ${ATTRIBUTE_OPTIONS} does not contain a valid json definition (actual: ${this.getAttribute( + ATTRIBUTE_OPTIONS, + )}).${e}`, + ); + } + } + + return {}; } /** @@ -1037,26 +1028,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); } /** @@ -1064,21 +1054,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; } /** @@ -1090,49 +1080,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; } /** @@ -1144,42 +1134,42 @@ 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)) { - const formatter = new Formatter(mapping); - if (this.getOption("templateFormatter.marker.open") !== null) { - formatter.setMarker( - this.getOption("templateFormatter.marker.open"), - this.getOption("templateFormatter.marker.close"), - ); - } - html = formatter.format(html); - } - - this.shadowRoot.innerHTML = html; - return this; + let template; + let html; + + try { + template = findDocumentTemplate(this.constructor.getTag()); + } catch (e) { + html = this.getOption("templates.main", ""); + if (!isString(html) || html === undefined || html === "") { + throw new Error("html is not set."); + } + } + + this.attachShadow({ + mode: this.getOption("shadowMode", "open"), + delegatesFocus: this.getOption("delegatesFocus", true), + }); + + if (template instanceof Template) { + this.shadowRoot.appendChild(template.createDocumentFragment()); + return this; + } + + const mapping = this.getOption("templateMapping", {}); + if (isObject(mapping)) { + const formatter = new Formatter(mapping); + if (this.getOption("templateFormatter.marker.open") !== null) { + formatter.setMarker( + this.getOption("templateFormatter.marker.open"), + this.getOption("templateFormatter.marker.close"), + ); + } + html = formatter.format(html); + } + + this.shadowRoot.innerHTML = html; + return this; } /** @@ -1193,20 +1183,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/error.mjs b/source/dom/error.mjs index dd18dfd1a23c77b8b2a86ca7e2763cc0a90be2fd..157d0bafbbf5aea910b5cc6855acc92ff4d37dc7 100644 --- a/source/dom/error.mjs +++ b/source/dom/error.mjs @@ -12,11 +12,10 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {validateInstance, validateString} from "../types/validate.mjs"; -import {ATTRIBUTE_ERRORMESSAGE} from "./constants.mjs"; +import { validateInstance, validateString } from "../types/validate.mjs"; +import { ATTRIBUTE_ERRORMESSAGE } from "./constants.mjs"; - -export {addErrorAttribute, removeErrorAttribute}; +export { addErrorAttribute, removeErrorAttribute }; /** * This method can be used to add an error message to an element. @@ -29,48 +28,45 @@ export {addErrorAttribute, removeErrorAttribute}; * @return {HTMLElement} */ function addErrorAttribute(element, message) { - validateInstance(element, HTMLElement); - - if(message instanceof Error) { - message = message.message; - } - - if(typeof message !== "string") { - if(typeof message === "object" && message !== null) { - if(typeof message.toString === "function") { - message = message.toString(); - } else { - message = JSON.stringify(message); - } - } else { - message = String(message); - } - } - - validateString(message); - - if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) { - element.setAttribute(ATTRIBUTE_ERRORMESSAGE, message); - return element; - } - - const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE); - const list = current.split("::"); - - for(let i = 0; i < list.length; i++) { - if(list[i] === message) { - return element; - } - } - - list.push(message); - - element.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - list.join("::") - ); - - return element; + validateInstance(element, HTMLElement); + + if (message instanceof Error) { + message = message.message; + } + + if (typeof message !== "string") { + if (typeof message === "object" && message !== null) { + if (typeof message.toString === "function") { + message = message.toString(); + } else { + message = JSON.stringify(message); + } + } else { + message = String(message); + } + } + + validateString(message); + + if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) { + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, message); + return element; + } + + const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE); + const list = current.split("::"); + + for (let i = 0; i < list.length; i++) { + if (list[i] === message) { + return element; + } + } + + list.push(message); + + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, list.join("::")); + + return element; } /** @@ -84,23 +80,20 @@ function addErrorAttribute(element, message) { * @return {HTMLElement} */ function removeErrorAttribute(element, message) { - validateInstance(element, HTMLElement); - validateString(message); - - if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) { - return element; - } - - const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE); - const list = current.split("::"); - const newList = list.filter(function (token) { - return token !== message; - }); - - element.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - newList.join("::") - ); - - return element; -} \ No newline at end of file + validateInstance(element, HTMLElement); + validateString(message); + + if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) { + return element; + } + + const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE); + const list = current.split("::"); + const newList = list.filter(function (token) { + return token !== message; + }); + + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, newList.join("::")); + + return element; +} diff --git a/source/monster.mjs b/source/monster.mjs index a287a0462c584e75644470d5dabe216770201f9f..a10c0e173faace8d0943a8de2a3892c40f3a79df 100644 --- a/source/monster.mjs +++ b/source/monster.mjs @@ -127,6 +127,7 @@ export * from "./constraints/isarray.mjs"; export * from "./constraints/abstract.mjs"; export * from "./constraints/valid.mjs"; export * from "./dom/dimension.mjs"; +export * from "./dom/error.mjs"; export * from "./dom/resource/link/stylesheet.mjs"; export * from "./dom/resource/link.mjs"; export * from "./dom/resource/script.mjs";