UNPKG

wj-elements

Version:

WebJET Elements is a modern set of user interface tools harnessing the power of web components designed to simplify web application development.

548 lines (547 loc) 27.9 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import WJElement from "./wje-element.js"; import { event } from "./event.js"; const styles = "/*\n[ WJ Input ]\n*/\n\n:host {\n width: 100%;\n margin-bottom: var(--wje-input-margin-bottom);\n display: block;\n label {\n margin: var(--wje-input-label-margin);\n padding: var(--wje-input-label-padding);\n display: var(--wje-input-label-display);\n opacity: 1;\n cursor: text;\n transition: opacity 0.2s ease;\n line-height: var(--wje-input-label-line-height);\n font-size: var(--wje-input-label-font-size);\n }\n .wrapper {\n display: grid;\n grid-template-columns: auto 1fr auto;\n width: 100%;\n > .input-wrapper {\n grid-column: 2;\n }\n }\n .native-input {\n .input-wrapper {\n display: block;\n width: 100%;\n position: relative;\n box-sizing: border-box;\n\n label {\n width: 100%;\n }\n }\n &.default {\n background-color: var(--wje-input-background-color);\n font-family: var(--wje-input-font-family);\n position: relative;\n border-radius: var(--wje-input-border-radius);\n border-width: var(--wje-input-border-width);\n border-style: var(--wje-input-border-style);\n border-color: var(--wje-input-border-color);\n padding-inline: 0;\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n transition: background-color 0.2s ease;\n cursor: text;\n .input-wrapper {\n padding-inline: 0.5rem;\n }\n &.focused {\n border-color: var(--wje-input-border-color-focus) !important;\n label {\n opacity: 0.67;\n font-size: 12px;\n letter-spacing: normal;\n }\n }\n input {\n border: none;\n height: 25px;\n min-height: 25px;\n padding: 0;\n margin-top: -4px;\n background: none;\n box-shadow: none;\n width: 100%;\n }\n label {\n &.fade {\n opacity: 0.5;\n font-size: 12px;\n letter-spacing: normal;\n }\n }\n ::slotted([slot='start']) {\n border-left: none;\n border-top: none;\n border-bottom: none;\n }\n\n ::slotted([slot='end']) {\n border-right: none;\n border-top: none;\n border-bottom: none;\n }\n }\n &.standard {\n font-family: var(--wje-input-font-family);\n position: relative;\n border-radius: var(--wje-input-border-radius);\n padding-inline: 0;\n padding-top: 0;\n padding-bottom: 0;\n transition: background-color 0.2s ease;\n cursor: text;\n &.focused {\n input {\n border-color: var(--wje-input-border-color-focus) !important;\n }\n }\n input {\n background-color: var(--wje-input-background-color);\n display: block;\n min-height: 32px;\n padding-inline: 0.5rem;\n padding-top: 0;\n padding-bottom: 0;\n /*background: none;*/\n box-shadow: none;\n width: 100%;\n box-sizing: border-box;\n border-radius: var(--wje-input-border-radius);\n border-width: var(--wje-input-border-width);\n border-style: var(--wje-input-border-style);\n border-color: var(--wje-input-border-color);\n }\n .input-wrapper {\n flex-wrap: nowrap;\n &:hover .clear {\n visibility: visible;\n }\n }\n ::slotted([slot='start']) {\n border-right: none;\n border-radius: var(--wje-input-border-radius) 0 0 var(--wje-input-border-radius);\n }\n\n ::slotted([slot='end']) {\n border-left: none;\n border-radius: 0 var(--wje-input-border-radius) var(--wje-input-border-radius) 0;\n }\n\n &.has-start input {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n\n &.has-end input {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n slot[name='error'] {\n position: static;\n\n background: transparent;\n padding: 0.25rem 0;\n left: auto;\n transform: none;\n color: var(--wje-input-color-invalid);\n font-size: 12px;\n line-height: normal;\n }\n }\n }\n}\n\n:host([type=hidden]) {\n margin: 0;\n .native-input {\n padding: 0;\n border-width: 0;\n }\n}\n\n.clear {\n visibility: hidden;\n position: absolute;\n right: 0;\n top: 3px;\n --wje-padding-top: 0.25rem;\n --wje-padding-start: 0.25rem;\n --wje-padding-end: 0.25rem;\n --wje-padding-bottom: 0.25rem;\n --wje-button-margin-inline: 0 0.25rem;\n}\n\n:host([required]) .wrapper::after {\n color: var(--wje-input-color-invalid);\n content: var(--wje-input-required-symbol);\n font-size: 24px;\n position: absolute;\n right: 12px;\n top: 0;\n}\n\n:host([required]) .standard .input-wrapper::after {\n right: 13px;\n top: -20px;\n}\n\n:host([invalid]) {\n .default {\n label {\n opacity: 1 !important;\n color: var(--wje-input-color-invalid) !important;\n animation-name: shake;\n animation-duration: 0.4s;\n animation-iteration-count: 1;\n }\n }\n}\n\n::slotted([slot='start']),\n::slotted([slot='end']) {\n display: flex;\n align-items: center;\n border-width: var(--wje-input-border-width);\n border-style: var(--wje-input-border-style);\n border-color: var(--wje-input-border-color);\n padding-inline: var(--wje-input-slot-padding-inline);\n}\n\n:host(.options-show) ::slotted([slot='start']) {\n border-bottom-left-radius: 0 !important;\n}\n\n:host(.options-show) ::slotted([slot='end']) {\n border-bottom-right-radius: 0 !important;\n}\n\nslot[name='start'],\nslot[name='end'] {\n display: flex;\n}\n\nslot[name='error'] {\n display: none;\n}\n\n:host([invalid]) slot[name='error'] {\n display: block;\n}\n\n:host([disabled]) input {\n opacity: 0.5;\n}\n\ninput {\n background-color: var(--wje-input-background-color);\n border-width: var(--wje-input-border-width);\n border-style: var(--wje-input-border-style);\n border-color: var(--wje-input-border-color);\n color: var(--wje-input-color);\n appearance: none;\n outline: 0;\n padding: 0.25rem 0.5rem;\n line-height: var(--wje-input-line-height);\n font-size: 14px;\n font-weight: normal;\n vertical-align: middle;\n min-height: 32px;\n}\n\nslot[name='error'] {\n display: none;\n position: absolute;\n max-width: 100%;\n min-width: auto;\n border-radius: 50px;\n background: black;\n padding: 0.25rem 0.5rem;\n top: 0;\n left: 50%;\n transform: translate(-50%, -50%);\n color: white;\n font-size: var(--wje-font-size-small);\n width: max-content;\n line-height: normal;\n}\n\n@keyframes shake {\n 8%,\n 41% {\n transform: translateX(-4px);\n }\n 25%,\n 58% {\n transform: translateX(4px);\n }\n 75% {\n transform: translateX(-2px);\n }\n 92% {\n transform: translateX(2px);\n }\n 0%,\n 100% {\n transform: translateX(0);\n }\n}\n"; class Input extends WJElement { /** * Creates an instance of Input. */ constructor() { super(); __publicField(this, "observeFunction", (mutations) => { mutations.forEach((mutation) => { if (mutation.type === "attributes") { const attributeName = mutation.attributeName; const oldValue = mutation.oldValue; const newValue = this.getAttribute(attributeName); console.log(`Attribute ${attributeName} changed from ${oldValue} to ${newValue}`); } }); }); /** * The class name of the input element. * @type {string} */ __publicField(this, "className", "Input"); this.invalid = false; this.pristine = true; this.internals = this.attachInternals(); this.observer = new MutationObserver(this.observeFunction); } /** * Setter for the value attribute. * @param {string} value The value to set. */ set value(value) { this.internals.setFormValue(value); if (this.input) this.input.value = value; this.pristine = false; this._value = value; } /** * Retrieves the value from the input element if available; otherwise, * returns the internal _value property or an empty string as the default. * @returns {string} The current value from the input element, the internal _value, or an empty string. */ get value() { var _a; return ((_a = this.input) == null ? void 0 : _a.value) ?? this._value ?? ""; } /** * Sets the label attribute of the element. * @param {string} value The value to set as the label attribute. */ set label(value) { this.setAttribute("label", value); } /** * Retrieves the value of the 'label' attribute if it exists. * If the 'label' attribute is not set, it returns false. * @returns {string|boolean} The value of the 'label' attribute as a string, or false if the attribute is not set. */ get label() { return this.getAttribute("label") || false; } /** * Sets the custom error display attribute for an element. * @param {boolean} value If true, adds the 'custom-error-display' attribute to the element. If false, removes the attribute from the element. */ set customErrorDisplay(value) { if (value) { this.setAttribute("custom-error-display", ""); } else { this.removeAttribute("custom-error-display"); } } /** * Getter for the customErrorDisplay attribute. * @returns {boolean} Whether the attribute is present. */ get customErrorDisplay() { return this.hasAttribute("custom-error-display"); } /** * Sets the `validateOnChange` property. If set to a truthy value, it adds the * `validate-on-change` attribute to the element. If set to a falsy value, it * removes the `validate-on-change` attribute from the element. * @param {boolean} value Determines whether to add or remove the * `validate-on-change` attribute. A truthy value adds the attribute, whereas a * falsy value removes it. */ set validateOnChange(value) { if (value) { this.setAttribute("validate-on-change", ""); } else { this.removeAttribute("validate-on-change"); } } /** * Getter for the validateOnChange attribute. * @returns {boolean} Whether the attribute is present. */ get validateOnChange() { return this.hasAttribute("validate-on-change"); } /** * Sets or removes the 'invalid' attribute on an element based on the provided value. * @param {boolean} value If true, the 'invalid' attribute is set. If false, the 'invalid' attribute is removed. */ set invalid(value) { if (value) { this.setAttribute("invalid", ""); } else { this.removeAttribute("invalid"); } } /** * Gets the value of the 'invalid' attribute. * Determines whether the 'invalid' attribute is present on the element. * @returns {boolean} True if the 'invalid' attribute is present, otherwise false. */ get invalid() { return this.hasAttribute("invalid"); } /** * Getter for the form attribute. * @returns {HTMLFormElement} The form the input is associated with. */ get form() { return this.internals.form; } /** * Getter for the name attribute. * @returns {string} The name of the input element. */ get name() { return this.getAttribute("name"); } /** * Getter for the validity attribute. * @returns {ValidityState} The validity state of the input. */ get validity() { return this.internals.validity; } /** * Getter for the validationMessage attribute. * @returns {string} The validation message of the input element. */ get validationMessage() { return this.internals.validationMessage; } /** * Getter for the willValidate attribute. * @returns {boolean} Whether the input will be validated. */ get willValidate() { return this.internals.willValidate; } /** * @summary Getter for the defaultValue attribute. * This method retrieves the 'value' attribute of the custom input element. * The 'value' attribute represents the default value of the input element. * If the 'value' attribute is not set, it returns an empty string. * @returns {string} The default value of the input element. */ get defaultValue() { return this.getAttribute("value") ?? ""; } /** * @summary Setter for the defaultValue attribute. * This method sets the 'value' attribute of the custom input element to the provided value. * The 'value' attribute represents the default value of the input element. * @param {string} value The value to set as the default value. */ set defaultValue(value) { this.setAttribute("value", value); } /** * Sets or removes the 'clearable' attribute on the element. * When set to a truthy value, the 'clearable' attribute is added; when falsy, the attribute is removed. * @param {boolean} value Determines whether to set or remove the 'clearable' attribute. If true, the 'clearable' attribute is added. If false, it is removed. */ set clearable(value) { if (value) { this.setAttribute("clearable", ""); } else { this.removeAttribute("clearable"); } } /** * Checks if the 'clearable' attribute is present on the element. * @returns {boolean} True if the 'clearable' attribute is set, otherwise false. */ get clearable() { return this.hasAttribute("clearable"); } /** * Sets the placeholder value for an element. If the provided value is non-empty, * it assigns the value to the "placeholder" attribute. Otherwise, it removes * the "placeholder" attribute from the element. * @param {string} value The placeholder text to set or null/undefined to remove the attribute. */ set placeholder(value) { if (value) { this.setAttribute("placeholder", value); } else { this.removeAttribute("placeholder"); } } /** * Retrieves the value of the 'placeholder' attribute from the element. * If the attribute is not set, it returns an empty string. * @returns {string} The value of the 'placeholder' attribute or an empty string if not set. */ get placeholder() { return this.getAttribute("placeholder") || ""; } /** * Sets the `variant` attribute on the element. If a value is provided, it will set the attribute to the given value. * If no value is provided, it removes the `variant` attribute from the element. * @param {string} value The value to set for the `variant` attribute. If falsy, the attribute is removed. */ set variant(value) { if (value) { this.setAttribute("variant", value); } else { this.removeAttribute("variant"); } } /** * Retrieves the value of the 'variant' attribute from the element. * If the attribute is not set, it defaults to 'default'. * @returns {string} The value of the 'variant' attribute or 'default' if not set. */ get variant() { return this.getAttribute("variant") || "default"; } /** * Getter for the cssStyleSheet attribute. * @returns {CSSStyleSheet} The CSS style sheet of the input element. */ static get cssStyleSheet() { return styles; } /** * Getter for the observedAttributes attribute of the input element. * @returns {Array} The attributes to observe for changes. */ static get observedAttributes() { return ["value", "name", "disabled", "placeholder", "label", "message", "error-inline"]; } /** * Sets up the attributes for the input. */ setupAttributes() { this.isShadowRoot = "open"; if (this.pristine) { this.value = this.defaultValue; this.pristine = false; } } beforeDraw() { this.observer.disconnect(); } /** * Draws the input element. * @returns {DocumentFragment} The drawn input. */ draw() { let hasSlotStart = this.hasSlot(this, "start"); let hasSlotEnd = this.hasSlot(this, "end"); this.hasSlot(this, "error"); let fragment = document.createDocumentFragment(); let native = document.createElement("div"); native.setAttribute("part", "native"); native.classList.add("native-input", this.variant); if (this.hasAttribute("invalid")) native.classList.add("has-error"); let wrapper = document.createElement("div"); wrapper.classList.add("wrapper"); let inputWrapper = document.createElement("div"); inputWrapper.setAttribute("part", "wrapper"); inputWrapper.classList.add("input-wrapper"); let label = document.createElement("label"); label.setAttribute("part", "label"); label.innerText = this.label; if (this.value && !this.hasAttribute("error")) label.classList.add("fade"); let input = document.createElement("input"); input.setAttribute("type", "text"); input.setAttribute("part", "input"); input.setAttribute("value", this.value || ""); input.classList.add("form-control"); const attributes = Array.from(this.attributes).map((attr) => attr.name); attributes.forEach((attr) => { if (this.hasAttribute(attr)) { input.setAttribute(attr, this[attr] || ""); } }); let error = document.createElement("div"); error.setAttribute("slot", "error"); let start = null; if (hasSlotStart) { start = document.createElement("slot"); start.setAttribute("name", "start"); start.setAttribute("part", "start"); } let end = null; if (hasSlotEnd) { end = document.createElement("slot"); end.setAttribute("name", "end"); end.setAttribute("part", "end"); } if (hasSlotStart) { wrapper.appendChild(start); native.classList.add("has-start"); } if (this.label) { if (this.variant === "standard") { native.append(label); } else { inputWrapper.appendChild(label); } } inputWrapper.appendChild(input); wrapper.appendChild(inputWrapper); native.appendChild(wrapper); let errorSlot = document.createElement("slot"); errorSlot.setAttribute("name", "error"); native.append(errorSlot); this.append(error); if (this.clearable) { this.clear = document.createElement("wje-button"); this.clear.classList.add("clear"); this.clear.setAttribute("fill", "link"); this.clear.setAttribute("part", "clear"); let clearIcon = document.createElement("wje-icon"); clearIcon.setAttribute("name", "x"); this.clear.appendChild(clearIcon); inputWrapper.appendChild(this.clear); } if (hasSlotEnd) { wrapper.appendChild(end); native.classList.add("has-end"); } fragment.appendChild(native); this.native = native; this.labelElement = label; this.input = input; this.errorMessage = error; return fragment; } /** * Runs after the input is drawn to the DOM. */ afterDraw() { this.input.addEventListener("focus", (e) => { this.labelElement.classList.add("fade"); this.native.classList.add("focused"); }); this.input.addEventListener("blur", (e) => { this.native.classList.remove("focused"); if (!e.target.value) this.labelElement.classList.remove("fade"); }); this.input.addEventListener("input", (e) => { this.validateInput(); if (this.validateOnChange) { this.pristine = false; this.propagateValidation(); } this.input.classList.remove("pristine"); this.labelElement.classList.add("fade"); const clone = new e.constructor(e.type, e); this.dispatchEvent(clone); event.dispatchCustomEvent(this, "wje-input:input", { value: this.input.value }); this.value = this.input.value; }); this.addEventListener("invalid", (e) => { this.invalid = true; this.pristine = false; this.showInvalidMessage(); if (this.customErrorDisplay) { e.preventDefault(); } }); this.addEventListener("focus", () => this.input.focus()); if (this.clear) { this.clear.addEventListener("wje-button:click", (e) => { this.input.value = ""; event.dispatchCustomEvent(this.clear, "wje-input:clear"); }); } this.validateInput(); if (this.hasAttribute("invalid")) { this.showInvalidMessage(); } this.observer.observe(this, { attributes: true, // Watch for attribute changes attributeOldValue: true // Keep track of the old value of attributes }); } componentCleanup() { this.observer.disconnect(); } /** * @summary Displays the validation message for the input. * If the input has a slot named 'error', it sets the text content of the element with attribute 'error-message' inside the slot to the validation message. * If the input does not have an 'error' slot, it sets the text content of the errorMessage property to the validation message. */ showInvalidMessage() { let hasSlotError = this.hasSlot(this, "error"); if (hasSlotError) { const slot = this.querySelector("[slot='error']"); let errorMessageEL = slot.querySelector("[error-message]"); if (!errorMessageEL) { const error = document.createElement("div"); error.setAttribute("error-message", ""); slot.append(error); errorMessageEL = error; } errorMessageEL.textContent = this.internals.validationMessage; } else { this.errorMessage.textContent = this.internals.validationMessage; } } /** * @summary Validates the input. * This method checks the validity state of the input. If the input is not valid, it iterates over the validity state object. * For each invalid state, it constructs an attribute name and checks if the input has this attribute. * If the input has the attribute, it sets the validation error to the state and the error message to the attribute value. * If the input does not have the attribute, it sets the error message to the default validation message of the input. * It then sets the validity in the form internals to an object with the validation error as key and true as value, and the error message. * If the input is valid, it sets the validity in the form internals to an empty object. */ validateInput() { const validState = this.input.validity; if (!validState.valid) { for (let state in validState) { const attr = `message-${state.toString()}`; if (validState[state]) { this.validationError = state.toString(); let errorMessage = this.message; if (!this.hasAttribute("message")) errorMessage = this.hasAttribute(attr) ? this.getAttribute(attr) : this.input.validationMessage; this.internals.setValidity({ [this.validationError]: true }, errorMessage); } } } else { this.internals.setValidity({}); } } /** * Checks and updates the validation state of the component based on its current properties. * If the component is invalid and a custom error display is enabled, it dispatches an 'invalid' event. * @returns {void} This method does not return a value. */ propagateValidation() { this.invalid = !this.pristine && !this.validity.valid; if (this.invalid) { event.dispatchCustomEvent(this, "invalid"); } } /** * Checks whether the input has a slot. * @param {HTMLElement} el The element to check. * @param {string} slotName The name of the slot to check for. * @returns {boolean} Whether the input has the slot. */ hasSlot(el, slotName = null) { let selector = slotName ? `[slot="${slotName}"]` : "[slot]"; return el.querySelectorAll(selector).length > 0 ? true : false; } /** * @summary Callback function that is called when the custom element is associated with a form. * This function adds an event listener to the form's submit event, which validates the input and propagates the validation. * @param {HTMLFormElement} form The form the custom element is associated with. */ formAssociatedCallback(form) { if (form) { this.internals.setFormValue(this.value); form == null ? void 0 : form.addEventListener("submit", () => { this.validateInput(); this.propagateValidation(); }); } } /** * The formResetCallback method is a built-in lifecycle callback that gets called when a form gets reset. * This method is responsible for resetting the value of the custom input element to its default value. * It also resets the form value and validity state in the form internals. * @function */ formResetCallback() { this.value = this.defaultValue; this.internals.setFormValue(this.defaultValue); this.internals.setValidity({}); } /** * The formStateRestoreCallback method is a built-in lifecycle callback that gets called when the state of a form-associated custom element is restored. * This method is responsible for restoring the value of the custom input element to its saved state. * It also restores the form value and validity state in the form internals to their saved states. * @param {object} state The saved state of the custom input element. * @function */ formStateRestoreCallback(state) { this.value = state.value; this.internals.setFormValue(state.value); this.internals.setValidity({}); } /** * The formStateSaveCallback method is a built-in lifecycle callback that gets called when the state of a form-associated custom element is saved. * This method is responsible for saving the value of the custom input element. * @returns {object} The saved state of the custom input element. * @function */ formStateSaveCallback() { return { value: this.value }; } /** * The formDisabledCallback method is a built-in lifecycle callback that gets called when the disabled state of a form-associated custom element changes. * This method is not implemented yet. * @param {boolean} disabled The new disabled state of the custom input element. * @function */ formDisabledCallback(disabled) { console.warn("formDisabledCallback not implemented yet"); } // dispatchEvent(e) { // return false; // } } /** * Whether the input is associated with a form. * @type {boolean} */ __publicField(Input, "formAssociated", true); Input.define("wje-input", Input); export { Input as default }; //# sourceMappingURL=wje-input.js.map