diff --git a/flake.lock b/flake.lock index 046146d078eea58db9a468ccf3124100168d5c01..f7ac676d6cde8b0596c73fb29bacdf49f6a9b314 100644 --- a/flake.lock +++ b/flake.lock @@ -25,6 +25,21 @@ "url": "https://gitlab.schukai.com/alvine/certificates.git" } }, + "common": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1, + "narHash": "sha256-J3hQz8ptyNNv+iGz6q0boMlQPnLWT2pDpGFi8aoo3Zk=", + "path": "./common", + "type": "path" + }, + "original": { + "path": "./common", + "type": "path" + } + }, "commonFlake": { "inputs": { "nixpkgs": "nixpkgs" @@ -47,24 +62,26 @@ }, "commonFlake_2": { "inputs": { + "common": "common", + "flake-utils": "flake-utils_2", + "git-commit": "git-commit", "nixpkgs": [ "nixpkgs" - ] + ], + "version": "version" }, "locked": { - "dir": "common", - "lastModified": 1718788884, - "narHash": "sha256-PefMbkGNMK9TN1qcNL9OkFVTNdv6wo6XoaS8eTdsY04=", + "lastModified": 1738246616, + "narHash": "sha256-3jleuvG7OUkVAIyszgQ3+8erCgA9KMt6REQEfgDCkCw=", "ref": "refs/heads/master", - "rev": "abda2dc723e13dfc835535593321c514666e679e", - "revCount": 39, + "rev": "ffd7bd5563d8e90aa6518a55fd18262a71a23690", + "revCount": 41, "type": "git", - "url": "https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git?dir=common" + "url": "https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git" }, "original": { - "dir": "common", "type": "git", - "url": "https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git?dir=common" + "url": "https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git" } }, "flake-utils": { @@ -102,6 +119,24 @@ "type": "github" } }, + "flake-utils_3": { + "inputs": { + "systems": "systems_4" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flakeUtils": { "inputs": { "systems": "systems_2" @@ -120,6 +155,24 @@ "type": "github" } }, + "git-commit": { + "inputs": { + "nixpkgs": [ + "commonFlake", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1, + "narHash": "sha256-ExMI4C4lehy6S+QOMORbNz/xftV3P2KDeFPmiq+x150=", + "path": "./git-commit", + "type": "path" + }, + "original": { + "path": "./git-commit", + "type": "path" + } + }, "nixpkgs": { "locked": { "lastModified": 1714971268, @@ -168,11 +221,26 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1737885640, - "narHash": "sha256-GFzPxJzTd1rPIVD4IW+GwJlyGwBDV1Tj5FLYwDQQ9sM=", + "lastModified": 1714971268, + "narHash": "sha256-IKwMSwHj9+ec660l+I4tki/1NRoeGpyA2GdtdYpAgEw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "27c13997bf450a01219899f5a83bd6ffbfc70d3c", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1738163270, + "narHash": "sha256-B/7Y1v4y+msFFBW1JAdFjNvVthvNdJKiN6EGRPnqfno=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4e96537f163fad24ed9eb317798a79afc85b51b7", + "rev": "59e618d90c065f55ae48446f307e8c09565d5ab0", "type": "github" }, "original": { @@ -186,8 +254,8 @@ "inputs": { "certificatesFlake": "certificatesFlake", "commonFlake": "commonFlake_2", - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_3", + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_4", "versionFlake": "versionFlake_2" } }, @@ -236,6 +304,42 @@ "type": "github" } }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "version": { + "inputs": { + "nixpkgs": [ + "commonFlake", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736378864, + "narHash": "sha256-7JSw/0AbjzM9jdkJQILEHKq8hjfTDB1tHJPLDI2oLVo=", + "ref": "refs/heads/master", + "rev": "b721b3d463b9f796b7652df59de975a362cc7542", + "revCount": 130, + "type": "git", + "url": "https://gitlab.schukai.com/oss/utilities/version.git" + }, + "original": { + "type": "git", + "url": "https://gitlab.schukai.com/oss/utilities/version.git" + } + }, "versionFlake": { "inputs": { "nixpkgs": "nixpkgs_2" diff --git a/flake.nix b/flake.nix index 55e5bf45a8a6feaa0f1d7cf6fb17e78dce5d962c..3f49545aca3fb35c2977951fc262d427a31502e5 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,7 @@ flake-utils = {url = "github:numtide/flake-utils";}; commonFlake = { - url = "git+https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git?dir=common"; + url = "git+https://gitlab.schukai.com/schukai/entwicklung/nix-flakes.git"; flake = true; inputs.nixpkgs.follows = "nixpkgs"; }; diff --git a/nix/config/release.nix b/nix/config/release.nix index dec5162b0b97e67b160275db5e4edca6c9dfc6bd..d4f06ca51cdba3d4a5d399283d1dcfed2c0ea8f1 100644 --- a/nix/config/release.nix +++ b/nix/config/release.nix @@ -3,4 +3,4 @@ commit = "83fc9e67ba531b29dce2a2aa8d07d29244d49765"; 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 8192779ce33196b8f7fd6396636dc3aa4b741f2c..1636e881152bec847ee2fdad3d463fff03e57571 100644 --- a/source/components/datatable/dataset.mjs +++ b/source/components/datatable/dataset.mjs @@ -278,7 +278,7 @@ class DataSet extends CustomElement { } initEventHandler.call(this); - },10); + }, 10); } /** diff --git a/source/dom/customcontrol.mjs b/source/dom/customcontrol.mjs index 5273c44232eec5426ddc7ea080f43b2dbfe11a38..02ed0363ff5bb9419cf7c0a82c4e8fd49bafc600 100644 --- a/source/dom/customcontrol.mjs +++ b/source/dom/customcontrol.mjs @@ -12,15 +12,15 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {extend} from "../data/extend.mjs"; -import {addAttributeToken} from "./attributes.mjs"; -import {ATTRIBUTE_ERRORMESSAGE} from "./constants.mjs"; -import {CustomElement, attributeObserverSymbol} from "./customelement.mjs"; -import {instanceSymbol} from "../constants.mjs"; -import {DeadMansSwitch} from "../util/deadmansswitch.mjs"; -import {addErrorAttribute} from "./error.mjs"; +import { extend } from "../data/extend.mjs"; +import { addAttributeToken } from "./attributes.mjs"; +import { ATTRIBUTE_ERRORMESSAGE } from "./constants.mjs"; +import { CustomElement, attributeObserverSymbol } from "./customelement.mjs"; +import { instanceSymbol } from "../constants.mjs"; +import { DeadMansSwitch } from "../util/deadmansswitch.mjs"; +import { addErrorAttribute } from "./error.mjs"; -export {CustomControl}; +export { CustomControl }; /** * @private @@ -53,286 +53,285 @@ const attachedInternalSymbol = Symbol("attachedInternal"); * @since 1.14.0 */ class CustomControl extends CustomElement { - /** - * The constructor method of CustomControl, which is called when creating a new instance. - * It checks whether the element supports `attachInternals()` and initializes an internal form-associated element - * if supported. Additionally, it initializes a MutationObserver to watch for attribute changes. - * - * See the links below for more information: - * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define|CustomElementRegistry.define()} - * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get|CustomElementRegistry.get()} - * and {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals|ElementInternals} - * - * @inheritdoc - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - constructor() { - super(); - - // check if element supports `attachInternals()` - if (typeof this["attachInternals"] === "function") { - this[attachedInternalSymbol] = this.attachInternals(); - } else { - // `attachInternals()` is not supported, so a polyfill is necessary - throw Error( - "the ElementInternals is not supported and a polyfill is necessary", - ); - } - - // watch for attribute value changes - initValueAttributeObserver.call(this); - } - - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/dom/custom-control@@instance"); - } - - /** - * This method determines which attributes are to be monitored by `attributeChangedCallback()`. - * - * @return {string[]} - * @since 1.15.0 - */ - static get observedAttributes() { - return super.observedAttributes; - } - - /** - * Adding a static `formAssociated` property, with a true value, makes an autonomous custom element a form-associated custom element. - * - * @see [attachInternals()]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} - * @see [Custom Elements Face Example]{@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example} - * @return {boolean} - */ - static formAssociated = true; - - /** - * @inheritdoc - **/ - get defaults() { - return extend({}, super.defaults); - } - - /** - * Must be overridden by a derived class and return the value of the control. - * - * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements. - * - * @throws {Error} the value getter must be overwritten by the derived class - */ - get value() { - throw Error("the value getter must be overwritten by the derived class"); - } - - /** - * Must be overridden by a derived class and set the value of the control. - * - * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements. - * - * @param {*} value The value to set. - * @throws {Error} the value setter must be overwritten by the derived class - */ - set value(value) { - throw Error("the value setter must be overwritten by the derived class"); - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {NodeList} - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/labels} - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - get labels() { - return getInternal.call(this)?.labels; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {string|null} - */ - get name() { - return this.getAttribute("name"); - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {string} - */ - get type() { - return this.constructor.getTag(); - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {ValidityState} - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - * @see [ValidityState]{@link https://developer.mozilla.org/en-US/docs/Web/API/ValidityState} - * @see [validity]{@link https://developer.mozilla.org/en-US/docs/Web/API/validity} - */ - get validity() { - return getInternal.call(this)?.validity; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {string} - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validationMessage - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - get validationMessage() { - return getInternal.call(this)?.validationMessage; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {boolean} - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/willValidate - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - get willValidate() { - return getInternal.call(this)?.willValidate; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {boolean} - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - get states() { - return getInternal.call(this)?.states; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {HTMLFontElement|null} - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/form - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - get form() { - return getInternal.call(this)?.form; - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * ``` - * // Use the control's name as the base name for submitted data - * const n = this.getAttribute('name'); - * const entries = new FormData(); - * entries.append(n + '-first-name', this.firstName_); - * entries.append(n + '-last-name', this.lastName_); - * this.setFormValue(entries); - * ``` - * - * @param {File|string|FormData} value - * @param {File|string|FormData} state - * @return {undefined} - * @throws {DOMException} NotSupportedError - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue - */ - setFormValue(value, state) { - getInternal.call(this).setFormValue(value, state); - } - - /** - * - * @param {object} flags - * @param {string|undefined} message - * @param {HTMLElement} anchor - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity - * @return {undefined} - * @throws {DOMException} NotSupportedError - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - setValidity(flags, message, anchor) { - getInternal.call(this).setValidity(flags, message, anchor); - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity - * @return {boolean} - * @throws {DOMException} NotSupportedError - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - */ - checkValidity() { - return getInternal.call(this)?.checkValidity(); - } - - /** - * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) - * - * @return {boolean} - * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity - * @throws {Error} the ElementInternals is not supported and a polyfill is necessary - * @throws {DOMException} NotSupportedError - */ - reportValidity() { - return getInternal.call(this)?.reportValidity(); - } - - /** - * Sets the `form` attribute of the custom control to the `id` of the passed form element. - * If no form element is passed, removes the `form` attribute. - * - * @param {HTMLFormElement} form - The form element to associate with the control - */ - formAssociatedCallback(form) { - if (form) { - if (form.id) { - this.setAttribute("form", form.id); - } - } else { - this.removeAttribute("form"); - } - } - - /** - * Sets or removes the `disabled` attribute of the custom control based on the passed value. - * - * @param {boolean} disabled - Whether or not the control should be disabled - */ - formDisabledCallback(disabled) { - if (disabled) { - if (!this.hasAttribute("disabled")) { - this.setAttribute("disabled", ""); - } - } else { - if (this.hasAttribute("disabled")) { - this.removeAttribute("disabled"); - } - } - } - - /** - * @param {string} state - * @param {string} mode - */ - formStateRestoreCallback(state, mode) { - } - - /** - * - */ - formResetCallback() { - this.value = ""; - } + /** + * The constructor method of CustomControl, which is called when creating a new instance. + * It checks whether the element supports `attachInternals()` and initializes an internal form-associated element + * if supported. Additionally, it initializes a MutationObserver to watch for attribute changes. + * + * See the links below for more information: + * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define|CustomElementRegistry.define()} + * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get|CustomElementRegistry.get()} + * and {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals|ElementInternals} + * + * @inheritdoc + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + constructor() { + super(); + + // check if element supports `attachInternals()` + if (typeof this["attachInternals"] === "function") { + this[attachedInternalSymbol] = this.attachInternals(); + } else { + // `attachInternals()` is not supported, so a polyfill is necessary + throw Error( + "the ElementInternals is not supported and a polyfill is necessary", + ); + } + + // watch for attribute value changes + initValueAttributeObserver.call(this); + } + + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + * @since 2.1.0 + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/dom/custom-control@@instance"); + } + + /** + * This method determines which attributes are to be monitored by `attributeChangedCallback()`. + * + * @return {string[]} + * @since 1.15.0 + */ + static get observedAttributes() { + return super.observedAttributes; + } + + /** + * Adding a static `formAssociated` property, with a true value, makes an autonomous custom element a form-associated custom element. + * + * @see [attachInternals()]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @see [Custom Elements Face Example]{@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example} + * @return {boolean} + */ + static formAssociated = true; + + /** + * @inheritdoc + **/ + get defaults() { + return extend({}, super.defaults); + } + + /** + * Must be overridden by a derived class and return the value of the control. + * + * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements. + * + * @throws {Error} the value getter must be overwritten by the derived class + */ + get value() { + throw Error("the value getter must be overwritten by the derived class"); + } + + /** + * Must be overridden by a derived class and set the value of the control. + * + * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements. + * + * @param {*} value The value to set. + * @throws {Error} the value setter must be overwritten by the derived class + */ + set value(value) { + throw Error("the value setter must be overwritten by the derived class"); + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {NodeList} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/labels} + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + get labels() { + return getInternal.call(this)?.labels; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {string|null} + */ + get name() { + return this.getAttribute("name"); + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {string} + */ + get type() { + return this.constructor.getTag(); + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {ValidityState} + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + * @see [ValidityState]{@link https://developer.mozilla.org/en-US/docs/Web/API/ValidityState} + * @see [validity]{@link https://developer.mozilla.org/en-US/docs/Web/API/validity} + */ + get validity() { + return getInternal.call(this)?.validity; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {string} + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validationMessage + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + get validationMessage() { + return getInternal.call(this)?.validationMessage; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {boolean} + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/willValidate + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + get willValidate() { + return getInternal.call(this)?.willValidate; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {boolean} + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + get states() { + return getInternal.call(this)?.states; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {HTMLFontElement|null} + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/form + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + get form() { + return getInternal.call(this)?.form; + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * ``` + * // Use the control's name as the base name for submitted data + * const n = this.getAttribute('name'); + * const entries = new FormData(); + * entries.append(n + '-first-name', this.firstName_); + * entries.append(n + '-last-name', this.lastName_); + * this.setFormValue(entries); + * ``` + * + * @param {File|string|FormData} value + * @param {File|string|FormData} state + * @return {undefined} + * @throws {DOMException} NotSupportedError + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue + */ + setFormValue(value, state) { + getInternal.call(this).setFormValue(value, state); + } + + /** + * + * @param {object} flags + * @param {string|undefined} message + * @param {HTMLElement} anchor + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity + * @return {undefined} + * @throws {DOMException} NotSupportedError + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + setValidity(flags, message, anchor) { + getInternal.call(this).setValidity(flags, message, anchor); + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity + * @return {boolean} + * @throws {DOMException} NotSupportedError + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + */ + checkValidity() { + return getInternal.call(this)?.checkValidity(); + } + + /** + * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) + * + * @return {boolean} + * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity + * @throws {Error} the ElementInternals is not supported and a polyfill is necessary + * @throws {DOMException} NotSupportedError + */ + reportValidity() { + return getInternal.call(this)?.reportValidity(); + } + + /** + * Sets the `form` attribute of the custom control to the `id` of the passed form element. + * If no form element is passed, removes the `form` attribute. + * + * @param {HTMLFormElement} form - The form element to associate with the control + */ + formAssociatedCallback(form) { + if (form) { + if (form.id) { + this.setAttribute("form", form.id); + } + } else { + this.removeAttribute("form"); + } + } + + /** + * Sets or removes the `disabled` attribute of the custom control based on the passed value. + * + * @param {boolean} disabled - Whether or not the control should be disabled + */ + formDisabledCallback(disabled) { + if (disabled) { + if (!this.hasAttribute("disabled")) { + this.setAttribute("disabled", ""); + } + } else { + if (this.hasAttribute("disabled")) { + this.removeAttribute("disabled"); + } + } + } + + /** + * @param {string} state + * @param {string} mode + */ + formStateRestoreCallback(state, mode) {} + + /** + * + */ + formResetCallback() { + this.value = ""; + } } /** @@ -342,13 +341,13 @@ class CustomControl extends CustomElement { * @this CustomControl */ function getInternal() { - if (!(attachedInternalSymbol in this)) { - throw new Error( - "ElementInternals is not supported and a polyfill is necessary", - ); - } + if (!(attachedInternalSymbol in this)) { + throw new Error( + "ElementInternals is not supported and a polyfill is necessary", + ); + } - return this[attachedInternalSymbol]; + return this[attachedInternalSymbol]; } const debounceValueSymbol = Symbol("debounceValue"); @@ -362,31 +361,29 @@ const debounceValueSymbol = Symbol("debounceValue"); * @this CustomControl */ function initValueAttributeObserver() { - const self = this; - this[attributeObserverSymbol]["value"] = () => { - - if (self[debounceValueSymbol] instanceof DeadMansSwitch) { - try { - self[debounceValueSymbol].touch(); - return; - } catch (e) { - if (e.message !== "has already run") { - throw e; - } - delete self[debounceValueSymbol]; - } - } - - self[debounceValueSymbol] = new DeadMansSwitch(10, () => { - const oldValue = self.getAttribute("value"); - const newValue = self.getOption("value"); - - if (oldValue !== newValue) { - setTimeout(() => { - this.setOption("value", this.getAttribute("value")); - },0); - - } - }); - }; + const self = this; + this[attributeObserverSymbol]["value"] = () => { + if (self[debounceValueSymbol] instanceof DeadMansSwitch) { + try { + self[debounceValueSymbol].touch(); + return; + } catch (e) { + if (e.message !== "has already run") { + throw e; + } + delete self[debounceValueSymbol]; + } + } + + self[debounceValueSymbol] = new DeadMansSwitch(10, () => { + const oldValue = self.getAttribute("value"); + const newValue = self.getOption("value"); + + if (oldValue !== newValue) { + setTimeout(() => { + this.setOption("value", this.getAttribute("value")); + }, 0); + } + }); + }; } diff --git a/source/dom/updater.mjs b/source/dom/updater.mjs index caf874e6b4b8a0771aa18fe7b46232657a5ff9fb..5f94b079f45f2dfaafee6bc9d611c629e2154dff 100644 --- a/source/dom/updater.mjs +++ b/source/dom/updater.mjs @@ -12,36 +12,39 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import {internalSymbol} from "../constants.mjs"; -import {diff} from "../data/diff.mjs"; -import {Pathfinder} from "../data/pathfinder.mjs"; -import {Pipe} from "../data/pipe.mjs"; +import { internalSymbol } from "../constants.mjs"; +import { diff } from "../data/diff.mjs"; +import { Pathfinder } from "../data/pathfinder.mjs"; +import { Pipe } from "../data/pipe.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_UPDATER_ATTRIBUTES, - ATTRIBUTE_UPDATER_BIND, - ATTRIBUTE_UPDATER_BIND_TYPE, - ATTRIBUTE_UPDATER_INSERT, - ATTRIBUTE_UPDATER_INSERT_REFERENCE, - ATTRIBUTE_UPDATER_REMOVE, - ATTRIBUTE_UPDATER_REPLACE, - ATTRIBUTE_UPDATER_SELECT_THIS, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_UPDATER_ATTRIBUTES, + ATTRIBUTE_UPDATER_BIND, + ATTRIBUTE_UPDATER_BIND_TYPE, + ATTRIBUTE_UPDATER_INSERT, + ATTRIBUTE_UPDATER_INSERT_REFERENCE, + ATTRIBUTE_UPDATER_REMOVE, + ATTRIBUTE_UPDATER_REPLACE, + ATTRIBUTE_UPDATER_SELECT_THIS, } from "./constants.mjs"; -import {Base} from "../types/base.mjs"; -import {isArray, isString, isInstance, isIterable} from "../types/is.mjs"; -import {Observer} from "../types/observer.mjs"; -import {ProxyObserver} from "../types/proxyobserver.mjs"; -import {validateArray, validateInstance} from "../types/validate.mjs"; -import {clone} from "../util/clone.mjs"; -import {trimSpaces} from "../util/trimspaces.mjs"; -import {addAttributeToken, addToObjectLink} from "./attributes.mjs"; -import {CustomElement, updaterTransformerMethodsSymbol} from "./customelement.mjs"; -import {findTargetElementFromEvent} from "./events.mjs"; -import {findDocumentTemplate} from "./template.mjs"; -import {getWindow} from "./util.mjs"; - -export {Updater, addObjectWithUpdaterToElement}; +import { Base } from "../types/base.mjs"; +import { isArray, isString, isInstance, isIterable } from "../types/is.mjs"; +import { Observer } from "../types/observer.mjs"; +import { ProxyObserver } from "../types/proxyobserver.mjs"; +import { validateArray, validateInstance } from "../types/validate.mjs"; +import { clone } from "../util/clone.mjs"; +import { trimSpaces } from "../util/trimspaces.mjs"; +import { addAttributeToken, addToObjectLink } from "./attributes.mjs"; +import { + CustomElement, + updaterTransformerMethodsSymbol, +} from "./customelement.mjs"; +import { findTargetElementFromEvent } from "./events.mjs"; +import { findDocumentTemplate } from "./template.mjs"; +import { getWindow } from "./util.mjs"; + +export { Updater, addObjectWithUpdaterToElement }; /** * The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be @@ -68,190 +71,190 @@ export {Updater, addObjectWithUpdaterToElement}; * @summary The updater class connects an object with the dom */ class Updater extends Base { - /** - * @since 1.8.0 - * @param {HTMLElement} element - * @param {object|ProxyObserver|undefined} subject - * @throws {TypeError} value is not a object - * @throws {TypeError} value is not an instance of HTMLElement - * @see {@link findDocumentTemplate} - */ - constructor(element, subject) { - super(); - - /** - * @type {HTMLElement} - */ - if (subject === undefined) subject = {}; - if (!isInstance(subject, ProxyObserver)) { - subject = new ProxyObserver(subject); - } - - this[internalSymbol] = { - element: validateInstance(element, HTMLElement), - last: {}, - callbacks: new Map(), - eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"], - subject: subject, - }; - - this[internalSymbol].callbacks.set( - "checkstate", - getCheckStateCallback.call(this), - ); - - this[internalSymbol].subject.attachObserver( - new Observer(() => { - const s = this[internalSymbol].subject.getRealSubject(); - - const diffResult = diff(this[internalSymbol].last, s); - this[internalSymbol].last = clone(s); - - const promises = []; - - for (const [, change] of Object.entries(diffResult)) { - promises.push( - new Promise((resolve, reject) => { - getWindow().requestAnimationFrame(() => { - try { - removeElement.call(this, change); - insertElement.call(this, change); - updateContent.call(this, change); - updateAttributes.call(this, change); - - resolve(); - } catch (error) { - reject(error); - } - }); - }), - ); - } - - return Promise.all(promises); - }), - ); - } - - /** - * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend' - * - * @see {@link https://developer.mozilla.org/de/docs/Web/Events} - * @since 1.9.0 - * @param {Array} types - * @return {Updater} - */ - setEventTypes(types) { - this[internalSymbol].eventTypes = validateArray(types); - return this; - } - - /** - * With this method, the eventlisteners are hooked in and the magic begins. - * - * ```js - * updater.run().then(() => { - * updater.enableEventProcessing(); - * }); - * ``` - * - * @since 1.9.0 - * @return {Updater} - * @throws {Error} the bind argument must start as a value with a path - */ - enableEventProcessing() { - this.disableEventProcessing(); - - for (const type of this[internalSymbol].eventTypes) { - // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - this[internalSymbol].element.addEventListener( - type, - getControlEventHandler.call(this), - { - capture: true, - passive: true, - }, - ); - } - - return this; - } - - /** - * This method turns off the magic or who loves it more profane it removes the eventListener. - * - * @since 1.9.0 - * @return {Updater} - */ - disableEventProcessing() { - for (const type of this[internalSymbol].eventTypes) { - this[internalSymbol].element.removeEventListener( - type, - getControlEventHandler.call(this), - ); - } - - return this; - } - - /** - * The run method must be called for the update to start working. - * The method ensures that changes are detected. - * - * ```js - * updater.run().then(() => { - * updater.enableEventProcessing(); - * }); - * ``` - * - * @summary Let the magic begin - * @return {Promise} - */ - run() { - // the key __init__has no further meaning and is only - // used to create the diff for empty objects. - this[internalSymbol].last = {__init__: true}; - return this[internalSymbol].subject.notifyObservers(); - } - - /** - * Gets the values of bound elements and changes them in subject - * - * @since 1.27.0 - * @return {Monster.DOM.Updater} - */ - retrieve() { - retrieveFromBindings.call(this); - return this; - } - - /** - * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here. - * However, if you passed a simple object, here you will get a proxy for that object. - * - * For changes, the ProxyObserver must be used. - * - * @since 1.8.0 - * @return {Proxy} - */ - getSubject() { - return this[internalSymbol].subject.getSubject(); - } - - /** - * This method can be used to register commands that can be called via call: instruction. - * This can be used to provide a pipe with its own functionality. - * - * @param {string} name - * @param {function} callback - * @return {Transformer} - * @throws {TypeError} value is not a string - * @throws {TypeError} value is not a function - */ - setCallback(name, callback) { - this[internalSymbol].callbacks.set(name, callback); - return this; - } + /** + * @since 1.8.0 + * @param {HTMLElement} element + * @param {object|ProxyObserver|undefined} subject + * @throws {TypeError} value is not a object + * @throws {TypeError} value is not an instance of HTMLElement + * @see {@link findDocumentTemplate} + */ + constructor(element, subject) { + super(); + + /** + * @type {HTMLElement} + */ + if (subject === undefined) subject = {}; + if (!isInstance(subject, ProxyObserver)) { + subject = new ProxyObserver(subject); + } + + this[internalSymbol] = { + element: validateInstance(element, HTMLElement), + last: {}, + callbacks: new Map(), + eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"], + subject: subject, + }; + + this[internalSymbol].callbacks.set( + "checkstate", + getCheckStateCallback.call(this), + ); + + this[internalSymbol].subject.attachObserver( + new Observer(() => { + const s = this[internalSymbol].subject.getRealSubject(); + + const diffResult = diff(this[internalSymbol].last, s); + this[internalSymbol].last = clone(s); + + const promises = []; + + for (const [, change] of Object.entries(diffResult)) { + promises.push( + new Promise((resolve, reject) => { + getWindow().requestAnimationFrame(() => { + try { + removeElement.call(this, change); + insertElement.call(this, change); + updateContent.call(this, change); + updateAttributes.call(this, change); + + resolve(); + } catch (error) { + reject(error); + } + }); + }), + ); + } + + return Promise.all(promises); + }), + ); + } + + /** + * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend' + * + * @see {@link https://developer.mozilla.org/de/docs/Web/Events} + * @since 1.9.0 + * @param {Array} types + * @return {Updater} + */ + setEventTypes(types) { + this[internalSymbol].eventTypes = validateArray(types); + return this; + } + + /** + * With this method, the eventlisteners are hooked in and the magic begins. + * + * ```js + * updater.run().then(() => { + * updater.enableEventProcessing(); + * }); + * ``` + * + * @since 1.9.0 + * @return {Updater} + * @throws {Error} the bind argument must start as a value with a path + */ + enableEventProcessing() { + this.disableEventProcessing(); + + for (const type of this[internalSymbol].eventTypes) { + // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + this[internalSymbol].element.addEventListener( + type, + getControlEventHandler.call(this), + { + capture: true, + passive: true, + }, + ); + } + + return this; + } + + /** + * This method turns off the magic or who loves it more profane it removes the eventListener. + * + * @since 1.9.0 + * @return {Updater} + */ + disableEventProcessing() { + for (const type of this[internalSymbol].eventTypes) { + this[internalSymbol].element.removeEventListener( + type, + getControlEventHandler.call(this), + ); + } + + return this; + } + + /** + * The run method must be called for the update to start working. + * The method ensures that changes are detected. + * + * ```js + * updater.run().then(() => { + * updater.enableEventProcessing(); + * }); + * ``` + * + * @summary Let the magic begin + * @return {Promise} + */ + run() { + // the key __init__has no further meaning and is only + // used to create the diff for empty objects. + this[internalSymbol].last = { __init__: true }; + return this[internalSymbol].subject.notifyObservers(); + } + + /** + * Gets the values of bound elements and changes them in subject + * + * @since 1.27.0 + * @return {Monster.DOM.Updater} + */ + retrieve() { + retrieveFromBindings.call(this); + return this; + } + + /** + * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here. + * However, if you passed a simple object, here you will get a proxy for that object. + * + * For changes, the ProxyObserver must be used. + * + * @since 1.8.0 + * @return {Proxy} + */ + getSubject() { + return this[internalSymbol].subject.getSubject(); + } + + /** + * This method can be used to register commands that can be called via call: instruction. + * This can be used to provide a pipe with its own functionality. + * + * @param {string} name + * @param {function} callback + * @return {Transformer} + * @throws {TypeError} value is not a string + * @throws {TypeError} value is not a function + */ + setCallback(name, callback) { + this[internalSymbol].callbacks.set(name, callback); + return this; + } } /** @@ -262,20 +265,20 @@ class Updater extends Base { * @this Updater */ function getCheckStateCallback() { - return function (current) { - // this is a reference to the current object (therefore no array function here) - if (this instanceof HTMLInputElement) { - if (["radio", "checkbox"].indexOf(this.type) !== -1) { - return `${this.value}` === `${current}` ? "true" : undefined; - } - } else if (this instanceof HTMLOptionElement) { - if (isArray(current) && current.indexOf(this.value) !== -1) { - return "true"; - } - - return undefined; - } - }; + return function (current) { + // this is a reference to the current object (therefore no array function here) + if (this instanceof HTMLInputElement) { + if (["radio", "checkbox"].indexOf(this.type) !== -1) { + return `${this.value}` === `${current}` ? "true" : undefined; + } + } else if (this instanceof HTMLOptionElement) { + if (isArray(current) && current.indexOf(this.value) !== -1) { + return "true"; + } + + return undefined; + } + }; } /** @@ -290,32 +293,32 @@ const symbol = Symbol("@schukai/monster/updater@@EventHandler"); * @throws {Error} the bind argument must start as a value with a path */ function getControlEventHandler() { - if (this[symbol]) { - return this[symbol]; - } - - /** - * @throws {Error} the bind argument must start as a value with a path. - * @throws {Error} unsupported object - * @param {Event} event - */ - this[symbol] = (event) => { - const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND); - - if (element === undefined) { - return; - } - - queueMicrotask(() => { - try { - retrieveAndSetValue.call(this, element); - } catch (e) { - addAttributeToken(element, ATTRIBUTE_ERRORMESSAGE, e.message || `${e}`); - } - }); - }; - - return this[symbol]; + if (this[symbol]) { + return this[symbol]; + } + + /** + * @throws {Error} the bind argument must start as a value with a path. + * @throws {Error} unsupported object + * @param {Event} event + */ + this[symbol] = (event) => { + const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND); + + if (element === undefined) { + return; + } + + queueMicrotask(() => { + try { + retrieveAndSetValue.call(this, element); + } catch (e) { + addAttributeToken(element, ATTRIBUTE_ERRORMESSAGE, e.message || `${e}`); + } + }); + }; + + return this[symbol]; } /** @@ -325,129 +328,128 @@ function getControlEventHandler() { * @private */ function retrieveAndSetValue(element) { - const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject()); - - let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND); - if (path === null) - throw new Error("the bind argument must start as a value with a path"); - - if (path.indexOf("path:") !== 0) { - throw new Error("the bind argument must start as a value with a path"); - } - - path = path.substring(5); // remove path: from the string - - let value; - - if (element instanceof HTMLInputElement) { - switch (element.type) { - case "checkbox": - value = element.checked ? element.value : undefined; - break; - default: - value = element.value; - break; - } - } else if (element instanceof HTMLTextAreaElement) { - value = element.value; - } else if (element instanceof HTMLSelectElement) { - switch (element.type) { - case "select-one": - value = element.value; - break; - case "select-multiple": - value = element.value; - - let options = element?.selectedOptions; - if (options === undefined) - options = element.querySelectorAll(":scope option:checked"); - value = Array.from(options).map(({value}) => value); - - break; - } - - // values from custom elements - } else if ( - (element?.constructor?.prototype && - !!Object.getOwnPropertyDescriptor( - element.constructor.prototype, - "value", - )?.["get"]) || - element.hasOwnProperty("value") - ) { - value = element?.["value"]; - } else { - throw new Error("unsupported object"); - } - - if (isString(value)) { - const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE); - switch (type) { - case "number": - case "int": - case "float": - case "integer": - value = Number(value); - if (isNaN(value)) { - value = 0; - } - break; - case "boolean": - case "bool": - case "checkbox": - value = value === "true" || value === "1" || value === "on"; - break; - - case "string[]": - if (value === "") { - value = []; - } - - value = value.split(",").map((v) => `${v}`); - break; - - case "int[]": - case "integer[]": - if (value === "") { - value = []; - } else { - value = value - .split(",") - .map((v) => { - try { - return parseInt(v, 10); - } catch (e) { - } - return -1; - }) - .filter((v) => v !== -1); - } - - break; - case "[]": - case "array": - case "list": - value = value.split(","); - break; - case "object": - case "json": - value = JSON.parse(value); - break; - default: - break; - } - } - - const copy = clone(this[internalSymbol].subject.getRealSubject()); - - const pf = new Pathfinder(copy); - pf.setVia(path, value); - - const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject()); - - if (diffResult.length > 0) { - pathfinder.setVia(path, value); - } + const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject()); + + let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND); + if (path === null) + throw new Error("the bind argument must start as a value with a path"); + + if (path.indexOf("path:") !== 0) { + throw new Error("the bind argument must start as a value with a path"); + } + + path = path.substring(5); // remove path: from the string + + let value; + + if (element instanceof HTMLInputElement) { + switch (element.type) { + case "checkbox": + value = element.checked ? element.value : undefined; + break; + default: + value = element.value; + break; + } + } else if (element instanceof HTMLTextAreaElement) { + value = element.value; + } else if (element instanceof HTMLSelectElement) { + switch (element.type) { + case "select-one": + value = element.value; + break; + case "select-multiple": + value = element.value; + + let options = element?.selectedOptions; + if (options === undefined) + options = element.querySelectorAll(":scope option:checked"); + value = Array.from(options).map(({ value }) => value); + + break; + } + + // values from custom elements + } else if ( + (element?.constructor?.prototype && + !!Object.getOwnPropertyDescriptor( + element.constructor.prototype, + "value", + )?.["get"]) || + element.hasOwnProperty("value") + ) { + value = element?.["value"]; + } else { + throw new Error("unsupported object"); + } + + if (isString(value)) { + const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE); + switch (type) { + case "number": + case "int": + case "float": + case "integer": + value = Number(value); + if (isNaN(value)) { + value = 0; + } + break; + case "boolean": + case "bool": + case "checkbox": + value = value === "true" || value === "1" || value === "on"; + break; + + case "string[]": + if (value === "") { + value = []; + } + + value = value.split(",").map((v) => `${v}`); + break; + + case "int[]": + case "integer[]": + if (value === "") { + value = []; + } else { + value = value + .split(",") + .map((v) => { + try { + return parseInt(v, 10); + } catch (e) {} + return -1; + }) + .filter((v) => v !== -1); + } + + break; + case "[]": + case "array": + case "list": + value = value.split(","); + break; + case "object": + case "json": + value = JSON.parse(value); + break; + default: + break; + } + } + + const copy = clone(this[internalSymbol].subject.getRealSubject()); + + const pf = new Pathfinder(copy); + pf.setVia(path, value); + + const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject()); + + if (diffResult.length > 0) { + pathfinder.setVia(path, value); + } } /** @@ -457,15 +459,15 @@ function retrieveAndSetValue(element) { * @private */ function retrieveFromBindings() { - if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) { - retrieveAndSetValue.call(this, this[internalSymbol].element); - } - - for (const [, element] of this[internalSymbol].element - .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`) - .entries()) { - retrieveAndSetValue.call(this, element); - } + if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) { + retrieveAndSetValue.call(this, this[internalSymbol].element); + } + + for (const [, element] of this[internalSymbol].element + .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`) + .entries()) { + retrieveAndSetValue.call(this, element); + } } /** @@ -476,11 +478,11 @@ function retrieveFromBindings() { * @return {void} */ function removeElement(change) { - for (const [, element] of this[internalSymbol].element - .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`) - .entries()) { - element.parentNode.removeChild(element); - } + for (const [, element] of this[internalSymbol].element + .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`) + .entries()) { + element.parentNode.removeChild(element); + } } /** @@ -496,133 +498,133 @@ function removeElement(change) { * @this Updater */ function insertElement(change) { - const subject = this[internalSymbol].subject.getRealSubject(); + const subject = this[internalSymbol].subject.getRealSubject(); - const mem = new WeakSet(); - let wd = 0; + const mem = new WeakSet(); + let wd = 0; - const container = this[internalSymbol].element; + const container = this[internalSymbol].element; - while (true) { - let found = false; - wd++; - - const p = clone(change?.["path"]); - if (!isArray(p)) return; - - while (p.length > 0) { - const current = p.join("."); - - let iterator = new Set(); - const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`; - - const e = container.querySelectorAll(query); - - if (e.length > 0) { - iterator = new Set([...e]); - } - - if (container.matches(query)) { - iterator.add(container); - } - - for (const [, containerElement] of iterator.entries()) { - if (mem.has(containerElement)) continue; - mem.add(containerElement); - - found = true; - - const attributes = containerElement.getAttribute( - ATTRIBUTE_UPDATER_INSERT, - ); - if (attributes === null) continue; - - const def = trimSpaces(attributes); - const i = def.indexOf(" "); - const key = trimSpaces(def.substr(0, i)); - const refPrefix = `${key}-`; - const cmd = trimSpaces(def.substr(i)); - - // this case is actually excluded by the query but is nevertheless checked again here - if (cmd.indexOf("|") > 0) { - throw new Error("pipes are not allowed when cloning a node."); - } - - const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f); - }); - - let value; - try { - containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } - - const dataPath = cmd.split(":").pop(); - - let insertPoint; - if (containerElement.hasChildNodes()) { - insertPoint = containerElement.lastChild; - } - - if (!isIterable(value)) { - throw new Error("the value is not iterable"); - } - - const available = new Set(); - - for (const [i] of Object.entries(value)) { - const ref = refPrefix + i; - const currentPath = `${dataPath}.${i}`; - - available.add(ref); - const refElement = containerElement.querySelector( - `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`, - ); - - if (refElement instanceof HTMLElement) { - insertPoint = refElement; - continue; - } - - appendNewDocumentFragment(containerElement, key, ref, currentPath); - } - - const nodes = containerElement.querySelectorAll( - `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`, - ); - - for (const [, node] of Object.entries(nodes)) { - if ( - !available.has( - node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE), - ) - ) { - try { - containerElement.removeChild(node); - } catch (e) { - containerElement.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ - e.message - }`.trim(), - ); - } - } - } - } - - p.pop(); - } - - if (found === false) break; - if (wd++ > 200) { - throw new Error("the maximum depth for the recursion is reached."); - } - } + while (true) { + let found = false; + wd++; + + const p = clone(change?.["path"]); + if (!isArray(p)) return; + + while (p.length > 0) { + const current = p.join("."); + + let iterator = new Set(); + const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`; + + const e = container.querySelectorAll(query); + + if (e.length > 0) { + iterator = new Set([...e]); + } + + if (container.matches(query)) { + iterator.add(container); + } + + for (const [, containerElement] of iterator.entries()) { + if (mem.has(containerElement)) continue; + mem.add(containerElement); + + found = true; + + const attributes = containerElement.getAttribute( + ATTRIBUTE_UPDATER_INSERT, + ); + if (attributes === null) continue; + + const def = trimSpaces(attributes); + const i = def.indexOf(" "); + const key = trimSpaces(def.substr(0, i)); + const refPrefix = `${key}-`; + const cmd = trimSpaces(def.substr(i)); + + // this case is actually excluded by the query but is nevertheless checked again here + if (cmd.indexOf("|") > 0) { + throw new Error("pipes are not allowed when cloning a node."); + } + + const pipe = new Pipe(cmd); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f); + }); + + let value; + try { + containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } + + const dataPath = cmd.split(":").pop(); + + let insertPoint; + if (containerElement.hasChildNodes()) { + insertPoint = containerElement.lastChild; + } + + if (!isIterable(value)) { + throw new Error("the value is not iterable"); + } + + const available = new Set(); + + for (const [i] of Object.entries(value)) { + const ref = refPrefix + i; + const currentPath = `${dataPath}.${i}`; + + available.add(ref); + const refElement = containerElement.querySelector( + `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`, + ); + + if (refElement instanceof HTMLElement) { + insertPoint = refElement; + continue; + } + + appendNewDocumentFragment(containerElement, key, ref, currentPath); + } + + const nodes = containerElement.querySelectorAll( + `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`, + ); + + for (const [, node] of Object.entries(nodes)) { + if ( + !available.has( + node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE), + ) + ) { + try { + containerElement.removeChild(node); + } catch (e) { + containerElement.setAttribute( + ATTRIBUTE_ERRORMESSAGE, + `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ + e.message + }`.trim(), + ); + } + } + } + } + + p.pop(); + } + + if (found === false) break; + if (wd++ > 200) { + throw new Error("the maximum depth for the recursion is reached."); + } + } } /** @@ -637,17 +639,17 @@ function insertElement(change) { * @throws {Error} no template was found with the specified key. */ function appendNewDocumentFragment(container, key, ref, path) { - const template = findDocumentTemplate(key, container); + const template = findDocumentTemplate(key, container); - const nodes = template.createDocumentFragment(); - for (const [, node] of Object.entries(nodes.childNodes)) { - if (node instanceof HTMLElement) { - applyRecursive(node, key, path); - node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref); - } + const nodes = template.createDocumentFragment(); + for (const [, node] of Object.entries(nodes.childNodes)) { + if (node instanceof HTMLElement) { + applyRecursive(node, key, path); + node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref); + } - container.appendChild(node); - } + container.appendChild(node); + } } /** @@ -660,27 +662,27 @@ function appendNewDocumentFragment(container, key, ref, path) { * @return {void} */ function applyRecursive(node, key, path) { - if (node instanceof HTMLElement) { - if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) { - const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE); - node.setAttribute( - ATTRIBUTE_UPDATER_REPLACE, - value.replaceAll(`path:${key}`, `path:${path}`), - ); - } - - if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { - const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); - node.setAttribute( - ATTRIBUTE_UPDATER_ATTRIBUTES, - value.replaceAll(`path:${key}`, `path:${path}`), - ); - } - - for (const [, child] of Object.entries(node.childNodes)) { - applyRecursive(child, key, path); - } - } + if (node instanceof HTMLElement) { + if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) { + const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE); + node.setAttribute( + ATTRIBUTE_UPDATER_REPLACE, + value.replaceAll(`path:${key}`, `path:${path}`), + ); + } + + if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { + const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); + node.setAttribute( + ATTRIBUTE_UPDATER_ATTRIBUTES, + value.replaceAll(`path:${key}`, `path:${path}`), + ); + } + + for (const [, child] of Object.entries(node.childNodes)) { + applyRecursive(child, key, path); + } + } } /** @@ -692,19 +694,19 @@ function applyRecursive(node, key, path) { * @this Updater */ function updateContent(change) { - const subject = this[internalSymbol].subject.getRealSubject(); - - const p = clone(change?.["path"]); - runUpdateContent.call(this, this[internalSymbol].element, p, subject); - - const slots = this[internalSymbol].element.querySelectorAll("slot"); - if (slots.length > 0) { - for (const [, slot] of Object.entries(slots)) { - for (const [, element] of Object.entries(slot.assignedNodes())) { - runUpdateContent.call(this, element, p, subject); - } - } - } + const subject = this[internalSymbol].subject.getRealSubject(); + + const p = clone(change?.["path"]); + runUpdateContent.call(this, this[internalSymbol].element, p, subject); + + const slots = this[internalSymbol].element.querySelectorAll("slot"); + if (slots.length > 0) { + for (const [, slot] of Object.entries(slots)) { + for (const [, element] of Object.entries(slot.assignedNodes())) { + runUpdateContent.call(this, element, p, subject); + } + } + } } /** @@ -717,69 +719,69 @@ function updateContent(change) { * @return {void} */ function runUpdateContent(container, parts, subject) { - if (!isArray(parts)) return; - if (!(container instanceof HTMLElement)) return; - parts = clone(parts); - - const mem = new WeakSet(); - - while (parts.length > 0) { - const current = parts.join("."); - parts.pop(); - - // Unfortunately, static data is always changed as well, since it is not possible to react to changes here. - const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`; - const e = container.querySelectorAll(`${query}`); - - const iterator = new Set([...e]); - - if (container.matches(query)) { - iterator.add(container); - } - - /** - * @type {HTMLElement} - */ - for (const [element] of iterator.entries()) { - if (mem.has(element)) return; - mem.add(element); - - const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE); - const cmd = trimSpaces(attributes); - - const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f); - }); - - let value; - try { - element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } - - if (value instanceof HTMLElement) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - try { - element.appendChild(value); - } catch (e) { - element.setAttribute( - ATTRIBUTE_ERRORMESSAGE, - `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ - e.message - }`.trim(), - ); - } - } else { - element.innerHTML = value; - } - } - } + if (!isArray(parts)) return; + if (!(container instanceof HTMLElement)) return; + parts = clone(parts); + + const mem = new WeakSet(); + + while (parts.length > 0) { + const current = parts.join("."); + parts.pop(); + + // Unfortunately, static data is always changed as well, since it is not possible to react to changes here. + const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`; + const e = container.querySelectorAll(`${query}`); + + const iterator = new Set([...e]); + + if (container.matches(query)) { + iterator.add(container); + } + + /** + * @type {HTMLElement} + */ + for (const [element] of iterator.entries()) { + if (mem.has(element)) return; + mem.add(element); + + const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE); + const cmd = trimSpaces(attributes); + + const pipe = new Pipe(cmd); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f); + }); + + let value; + try { + element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } + + if (value instanceof HTMLElement) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + try { + element.appendChild(value); + } catch (e) { + element.setAttribute( + ATTRIBUTE_ERRORMESSAGE, + `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${ + e.message + }`.trim(), + ); + } + } else { + element.innerHTML = value; + } + } + } } /** @@ -789,9 +791,9 @@ function runUpdateContent(container, parts, subject) { * @return {void} */ function updateAttributes(change) { - const subject = this[internalSymbol].subject.getRealSubject(); - const p = clone(change?.["path"]); - runUpdateAttributes.call(this, this[internalSymbol].element, p, subject); + const subject = this[internalSymbol].subject.getRealSubject(); + const p = clone(change?.["path"]); + runUpdateAttributes.call(this, this[internalSymbol].element, p, subject); } /** @@ -803,70 +805,70 @@ function updateAttributes(change) { * @this Updater */ function runUpdateAttributes(container, parts, subject) { - if (!isArray(parts)) return; - parts = clone(parts); + if (!isArray(parts)) return; + parts = clone(parts); - const mem = new WeakSet(); + const mem = new WeakSet(); - while (parts.length > 0) { - const current = parts.join("."); - parts.pop(); + while (parts.length > 0) { + const current = parts.join("."); + parts.pop(); - let iterator = new Set(); + let iterator = new Set(); - const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`; + const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`; - const e = container.querySelectorAll(query); + const e = container.querySelectorAll(query); - if (e.length > 0) { - iterator = new Set([...e]); - } + if (e.length > 0) { + iterator = new Set([...e]); + } - if (container.matches(query)) { - iterator.add(container); - } + if (container.matches(query)) { + iterator.add(container); + } - for (const [element] of iterator.entries()) { - if (mem.has(element)) return; - mem.add(element); + for (const [element] of iterator.entries()) { + if (mem.has(element)) return; + mem.add(element); - // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set - if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { - continue; - } + // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set + if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) { + continue; + } - const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); + const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES); - for (let [, def] of Object.entries(attributes.split(","))) { - def = trimSpaces(def); - const i = def.indexOf(" "); - const name = trimSpaces(def.substr(0, i)); - const cmd = trimSpaces(def.substr(i)); + for (let [, def] of Object.entries(attributes.split(","))) { + def = trimSpaces(def); + const i = def.indexOf(" "); + const name = trimSpaces(def.substr(0, i)); + const cmd = trimSpaces(def.substr(i)); - const pipe = new Pipe(cmd); + const pipe = new Pipe(cmd); - this[internalSymbol].callbacks.forEach((f, n) => { - pipe.setCallback(n, f, element); - }); + this[internalSymbol].callbacks.forEach((f, n) => { + pipe.setCallback(n, f, element); + }); - let value; - try { - element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); - value = pipe.run(subject); - } catch (e) { - element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); - } + let value; + try { + element.removeAttribute(ATTRIBUTE_ERRORMESSAGE); + value = pipe.run(subject); + } catch (e) { + element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message); + } - if (value === undefined) { - element.removeAttribute(name); - } else if (element.getAttribute(name) !== value) { - element.setAttribute(name, value); - } + if (value === undefined) { + element.removeAttribute(name); + } else if (element.getAttribute(name) !== value) { + element.setAttribute(name, value); + } - handleInputControlAttributeUpdate.call(this, element, name, value); - } - } - } + handleInputControlAttributeUpdate.call(this, element, name, value); + } + } + } } /** @@ -879,54 +881,52 @@ function runUpdateAttributes(container, parts, subject) { */ function handleInputControlAttributeUpdate(element, name, value) { - - if (element instanceof HTMLSelectElement) { - switch (element.type) { - case "select-multiple": - for (const [index, opt] of Object.entries(element.options)) { - opt.selected = value.indexOf(opt.value) !== -1; - } - - break; - case "select-one": - // Only one value may be selected - - for (const [index, opt] of Object.entries(element.options)) { - if (opt.value === value) { - element.selectedIndex = index; - break; - } - } - - break; - } - } else if (element instanceof HTMLInputElement) { - switch (element.type) { - case "radio": - if (name === "checked") { - element.checked = value !== undefined; - } - break; - - case "checkbox": - if (name === "checked") { - element.checked = value !== undefined; - } - break; - - case "text": - default: - if (name === "value") { - element.value = value === undefined ? "" : value; - } - break; - - } - } else if (element instanceof HTMLTextAreaElement) { - if (name === "value") { - element.value = value === undefined ? "" : value; - } - } + if (element instanceof HTMLSelectElement) { + switch (element.type) { + case "select-multiple": + for (const [index, opt] of Object.entries(element.options)) { + opt.selected = value.indexOf(opt.value) !== -1; + } + + break; + case "select-one": + // Only one value may be selected + + for (const [index, opt] of Object.entries(element.options)) { + if (opt.value === value) { + element.selectedIndex = index; + break; + } + } + + break; + } + } else if (element instanceof HTMLInputElement) { + switch (element.type) { + case "radio": + if (name === "checked") { + element.checked = value !== undefined; + } + break; + + case "checkbox": + if (name === "checked") { + element.checked = value !== undefined; + } + break; + + case "text": + default: + if (name === "value") { + element.value = value === undefined ? "" : value; + } + break; + } + } else if (element instanceof HTMLTextAreaElement) { + if (name === "value") { + element.value = value === undefined ? "" : value; + } + } } /** @@ -945,83 +945,83 @@ function handleInputControlAttributeUpdate(element, name, value) { * @throws {TypeError} symbol must be an instance of Symbol */ function addObjectWithUpdaterToElement(elements, symbol, object, config = {}) { - if (!(this instanceof HTMLElement)) { - throw new TypeError( - "the context of this function must be an instance of HTMLElement", - ); - } - - if (!(typeof symbol === "symbol")) { - throw new TypeError("symbol must be an instance of Symbol"); - } - - const updaters = new Set(); - - if (elements instanceof NodeList) { - elements = new Set([...elements]); - } else if (elements instanceof HTMLElement) { - elements = new Set([elements]); - } else if (elements instanceof Set) { - } else { - throw new TypeError( - `elements is not a valid type. (actual: ${typeof elements})`, - ); - } - - const result = []; - - const updaterCallbacks = []; - const cb = this?.[updaterTransformerMethodsSymbol]; - if (this instanceof HTMLElement && typeof cb === "function") { - const callbacks = cb.call(this); - if (typeof callbacks === "object") { - for (const [name, callback] of Object.entries(callbacks)) { - if (typeof callback === "function") { - updaterCallbacks.push([name, callback]); - } else { - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - `onUpdaterPipeCallbacks: ${name} is not a function`, - ); - } - } - } else { - addAttributeToken( - this, - ATTRIBUTE_ERRORMESSAGE, - `onUpdaterPipeCallbacks do not return an object with functions`, - ); - } - } - - elements.forEach((element) => { - if (!(element instanceof HTMLElement)) return; - if (element instanceof HTMLTemplateElement) return; - - const u = new Updater(element, object); - updaters.add(u); - - if (updaterCallbacks.length > 0) { - for (const [name, callback] of updaterCallbacks) { - u.setCallback(name, callback); - } - } - - result.push( - u.run().then(() => { - if (config.eventProcessing === true) { - u.enableEventProcessing(); - } - - return u; - }), - ); - }); - - if (updaters.size > 0) { - addToObjectLink(this, symbol, updaters); - } - - return result; + if (!(this instanceof HTMLElement)) { + throw new TypeError( + "the context of this function must be an instance of HTMLElement", + ); + } + + if (!(typeof symbol === "symbol")) { + throw new TypeError("symbol must be an instance of Symbol"); + } + + const updaters = new Set(); + + if (elements instanceof NodeList) { + elements = new Set([...elements]); + } else if (elements instanceof HTMLElement) { + elements = new Set([elements]); + } else if (elements instanceof Set) { + } else { + throw new TypeError( + `elements is not a valid type. (actual: ${typeof elements})`, + ); + } + + const result = []; + + const updaterCallbacks = []; + const cb = this?.[updaterTransformerMethodsSymbol]; + if (this instanceof HTMLElement && typeof cb === "function") { + const callbacks = cb.call(this); + if (typeof callbacks === "object") { + for (const [name, callback] of Object.entries(callbacks)) { + if (typeof callback === "function") { + updaterCallbacks.push([name, callback]); + } else { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + `onUpdaterPipeCallbacks: ${name} is not a function`, + ); + } + } + } else { + addAttributeToken( + this, + ATTRIBUTE_ERRORMESSAGE, + `onUpdaterPipeCallbacks do not return an object with functions`, + ); + } + } + + elements.forEach((element) => { + if (!(element instanceof HTMLElement)) return; + if (element instanceof HTMLTemplateElement) return; + + const u = new Updater(element, object); + updaters.add(u); + + if (updaterCallbacks.length > 0) { + for (const [name, callback] of updaterCallbacks) { + u.setCallback(name, callback); + } + } + + result.push( + u.run().then(() => { + if (config.eventProcessing === true) { + u.enableEventProcessing(); + } + + return u; + }), + ); + }); + + if (updaters.size > 0) { + addToObjectLink(this, symbol, updaters); + } + + return result; }