Skip to content
Snippets Groups Projects
Select Git revision
  • 8d49ffea20934e03ae8f871e17ddbecbb5b4320e
  • master default protected
  • 1.31
  • 4.24.3
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
23 results

Monster.Constraints.AbstractConstraint.html

Blame
  • customcontrol.mjs 12.72 KiB
    /**
     * 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";
    import { DeadMansSwitch } from "../util/deadmansswitch.mjs";
    import { addErrorAttribute } from "./error.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",
    			);
    		}
    
    		// 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 = "";
    	}
    }
    
    /**
     * @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];
    }
    
    const debounceValueSymbol = Symbol("debounceValue");
    
    /**
     *
     * @issue https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/290
     *
     * @private
     * @return {object}
     * @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);
    			}
    		});
    	};
    }