215 lines
5.3 KiB
JavaScript
215 lines
5.3 KiB
JavaScript
/**
|
|
* An abstract custom HTMLElement designed for use with form inputs.
|
|
* @abstract
|
|
* @template {any} FormInputValueType
|
|
*
|
|
* @fires {Event} input An "input" event when the value of the input changes
|
|
* @fires {Event} change A "change" event when the value of the element changes
|
|
*/
|
|
export default class AbstractFormInputElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._internals = this.attachInternals();
|
|
}
|
|
|
|
/**
|
|
* The HTML tag name used by this element.
|
|
* @type {string}
|
|
*/
|
|
static tagName;
|
|
|
|
/**
|
|
* Declare that this custom element provides form element functionality.
|
|
* @type {boolean}
|
|
*/
|
|
static formAssociated = true;
|
|
|
|
/**
|
|
* Attached ElementInternals which provides form handling functionality.
|
|
* @type {ElementInternals}
|
|
* @protected
|
|
*/
|
|
_internals;
|
|
|
|
/**
|
|
* The primary input (if any). Used to determine what element should receive focus when an associated label is clicked
|
|
* on.
|
|
* @type {HTMLElement}
|
|
* @protected
|
|
*/
|
|
_primaryInput;
|
|
|
|
/**
|
|
* The form this element belongs to.
|
|
* @type {HTMLFormElement}
|
|
*/
|
|
get form() {
|
|
return this._internals.form;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Element Properties */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* The input element name.
|
|
* @type {string}
|
|
*/
|
|
get name() {
|
|
return this.getAttribute("name");
|
|
}
|
|
|
|
set name(value) {
|
|
this.setAttribute("name", value);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* The value of the input element.
|
|
* @type {FormInputValueType}
|
|
*/
|
|
get value() {
|
|
return this._getValue();
|
|
}
|
|
|
|
set value(value) {
|
|
this._setValue(value);
|
|
this.dispatchEvent(new Event("input", {bubbles: true, cancelable: true}));
|
|
this.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
|
|
this._refresh();
|
|
}
|
|
|
|
/**
|
|
* The underlying value of the element.
|
|
* @type {FormInputValueType}
|
|
* @protected
|
|
*/
|
|
_value;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Return the value of the input element which should be submitted to the form.
|
|
* @returns {FormInputValueType}
|
|
* @protected
|
|
*/
|
|
_getValue() {
|
|
return this._value;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Translate user-provided input value into the format that should be stored.
|
|
* @param {FormInputValueType} value A new value to assign to the element
|
|
* @throws {Error} An error if the provided value is invalid
|
|
* @protected
|
|
*/
|
|
_setValue(value) {
|
|
this._value = value;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Is this element disabled?
|
|
* @type {boolean}
|
|
*/
|
|
get disabled() {
|
|
return this.hasAttribute("disabled");
|
|
}
|
|
|
|
set disabled(value) {
|
|
this.toggleAttribute("disabled", value);
|
|
this._toggleDisabled(!this.editable);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Is this field editable? The field can be neither disabled nor readonly.
|
|
* @type {boolean}
|
|
*/
|
|
get editable() {
|
|
return !(this.hasAttribute("disabled") || this.hasAttribute("readonly"));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Special behaviors that the subclass should implement when toggling the disabled state of the input.
|
|
* @param {boolean} disabled The new disabled state
|
|
* @protected
|
|
*/
|
|
_toggleDisabled(disabled) {}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Element Lifecycle */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Initialize the custom element, constructing its HTML.
|
|
*/
|
|
connectedCallback() {
|
|
const elements = this._buildElements();
|
|
this.replaceChildren(...elements);
|
|
this._refresh();
|
|
this._toggleDisabled(!this.editable);
|
|
this.addEventListener("click", this._onClick.bind(this));
|
|
this._activateListeners();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Create the HTML elements that should be included in this custom element.
|
|
* Elements are returned as an array of ordered children.
|
|
* @returns {HTMLElement[]}
|
|
* @protected
|
|
*/
|
|
_buildElements() {
|
|
return [];
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Refresh the active state of the custom element.
|
|
* @protected
|
|
*/
|
|
_refresh() {}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Apply key attributes on the containing custom HTML element to input elements contained within it.
|
|
* @internal
|
|
*/
|
|
_applyInputAttributes(input) {
|
|
input.toggleAttribute("required", this.hasAttribute("required"));
|
|
input.toggleAttribute("disabled", this.hasAttribute("disabled"));
|
|
input.toggleAttribute("readonly", this.hasAttribute("readonly"));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Activate event listeners which add dynamic behavior to the custom element.
|
|
* @protected
|
|
*/
|
|
_activateListeners() {}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Special handling when the custom element is clicked. This should be implemented to transfer focus to an
|
|
* appropriate internal element.
|
|
* @param {PointerEvent} event
|
|
* @protected
|
|
*/
|
|
_onClick(event) {
|
|
if ( event.target === this ) this._primaryInput?.focus?.();
|
|
}
|
|
}
|