Something went wrong on our end
Select Git revision
pathfind.go
-
Volker Schukai authoredVolker Schukai authored
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);
}
});
};
}