Initial
This commit is contained in:
214
resources/app/client-esm/applications/elements/form-element.mjs
Normal file
214
resources/app/client-esm/applications/elements/form-element.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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?.();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user