/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import { 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"; export { CustomControl }; /** * @private * @type {symbol} */ const attachedInternalSymbol = Symbol("attachedInternal"); /** * This is a base class for creating custom controls using the power of CustomElement. * * After defining a `CustomElement`, the `registerCustomElement` method must be called with the new class name. Only then * will the tag defined via the `getTag` method be made known to the DOM. * * This control uses `attachInternals()` to integrate the control into a form. If the target environment does not support * this method, the [polyfill](https://www.npmjs.com/package/element-internals-polyfill) can be used. * * You can create the object using the function `document.createElement()`. * * This control uses `attachInternals()` to integrate the control into a form. If the target environment does not support * this method, the Polyfill for attachInternals() can be used: {@link https://www.npmjs.com/package/element-internals-polyfill|element-internals-polyfill}. * * Learn more about WICG Web Components: {@link https://github.com/WICG/webcomponents|WICG Web Components}. * * Read the HTML specification for Custom Elements: {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements|Custom Elements}. * * Read the HTML specification for Custom Element Reactions: {@link https://html.spec.whatwg.org/dev/custom-elements.html#custom-element-reactions|Custom Element Reactions}. * * @summary A base class for custom controls based on CustomElement. * @license AGPLv3 * @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", ); } // initialize a MutationObserver to watch for attribute changes initObserver.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 = ""; } } /** * @private * @return {object} * @throws {Error} the ElementInternals is not supported and a polyfill is necessary * @this CustomControl */ function getInternal() { if (!(attachedInternalSymbol in this)) { throw new Error( "ElementInternals is not supported and a polyfill is necessary", ); } return this[attachedInternalSymbol]; } /** * @private * @return {object} * @this CustomControl */ function initObserver() { // value this[attributeObserverSymbol]["value"] = () => { this.setOption("value", this.getAttribute("value")); }; }