UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

373 lines (324 loc) 8.5 kB
/** * 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. */ import { instanceSymbol } from "../../constants.mjs"; import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { DigitsStyleSheet } from "./stylesheet/digits.mjs"; import { addErrorAttribute } from "../../dom/error.mjs"; import { Observer } from "../../types/observer.mjs"; import { CustomControl } from "../../dom/customcontrol.mjs"; import { InvalidStyleSheet } from "./stylesheet/invalid.mjs"; export { Digits }; /** * @private * @type {symbol} */ const digitsElementSymbol = Symbol("digitsElement"); /** * A Digits * * @fragments /fragments/components/form/digits/ * * @example /examples/components/form/digits-simple * * @since 3.113.0 * @copyright schukai GmbH * @summary A beautiful Digits that can make your life easier and also looks good. */ class Digits extends CustomControl { /** * */ constructor() { super(); initOptionObserver.call(this); } /** * This method is called by the `instanceof` operator. * @returns {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/form/digits@@instance"); } /** * * @return {Components.Form.Digits */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); setTimeout(() => { initControlReferences.call(this); initEventHandler.call(this); updateDigitControls.call(this); }, 0); return this; } /** * The current value of the Switch * * ``` * e = document.querySelector('monster-toggle-switch'); * console.log(e.value) * // ↦ on * ``` * * @return {string} */ get value() { return this.getOption("value"); } /** * Set value * * ``` * e = document.querySelector('monster-toggle-switch'); * e.value="on" * ``` * * @property {string} value */ set value(value) { const chars = String(value).split(""); this.setOption("value", value); this.setFormValue(value); if (chars.every(checkCharacter.bind(this))) { this.setValidity({ badInput: false }, ""); } else { this.setValidity( { badInput: true }, "The value contains invalid characters", ); } } /** * To set the options via the HTML Tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {number} digits Number of digits * @property {string} characterSet Character set for the digits, which are allowed */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, digits: 4, characterSet: "0123456789", digitsControls: [], value: null, }); } /** * @return {string} */ static getTag() { return "monster-digits"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [DigitsStyleSheet, InvalidStyleSheet]; } /** * @return {void} */ focus(options) { const element = this[digitsElementSymbol].querySelector("input"); if (element) { element.focus(options); } } /** * @return {void} */ blur() { const elements = this[digitsElementSymbol].querySelectorAll("input"); if (elements) { for (const element of elements) { element.blur(); } } } } /** * @private */ function initOptionObserver() { const self = this; let lastValue = this.getOption("value"); self.attachObserver( new Observer(function () { if (lastValue !== self.getOption("value")) { lastValue = self.getOption("value"); updateDigitControls.call(self); } }), ); } /** * @private */ function updateDigitControls() { const digits = this.getOption("digits") || this.setOption("digits", 4); const controls = []; const values = this.getOption("value") || ""; if (this[digitsElementSymbol]) { this[digitsElementSymbol].style.gridTemplateColumns = `repeat(${digits}, 1fr)`; } for (let i = 0; i < digits; i++) { controls.push({ value: values[i] ?? " ", }); } this.setOption("digitsControls", controls); } /** * @private * @param value * @returns {boolean} */ function checkCharacter(value) { const characterSet = this.getOption("characterSet"); return characterSet.includes(value); } /** * @private * @returns {initEventHandler} */ function initEventHandler() { const self = this; const element = this[digitsElementSymbol]; element.addEventListener("keydown", function (event) { if (event.target.tagName !== "INPUT") return; const inputControl = event.target; const pressedKey = event.key; if ((event.ctrlKey || event.metaKey) && pressedKey === "v") { console.log("paste"); event.preventDefault(); navigator.clipboard .readText() .then((clipText) => { self.value = clipText; }) .catch(() => { addErrorAttribute(this, "Error while pasting"); }); return; } if (pressedKey === "ArrowRight") { const nextControl = inputControl.nextElementSibling; if (nextControl && nextControl.tagName === "INPUT") { event.preventDefault(); nextControl.focus(); } return; } if (pressedKey === "ArrowLeft") { const previousControl = inputControl.previousElementSibling; if (previousControl && previousControl.tagName === "INPUT") { event.preventDefault(); previousControl.focus(); } return; } if (pressedKey === "Backspace") { if (inputControl.value !== "" && inputControl.value !== " ") { event.preventDefault(); inputControl.value = ""; collectValues.call(self); return; } else { const previousControl = inputControl.previousElementSibling; if (previousControl && previousControl.tagName === "INPUT") { event.preventDefault(); previousControl.focus(); } return; } } if (pressedKey.length === 1) { if (!checkCharacter.call(self, pressedKey)) { event.preventDefault(); inputControl.classList.add("invalid"); setTimeout(() => { inputControl.classList.remove("invalid"); }, 500); return; } if (inputControl.value.length === 1) { event.preventDefault(); inputControl.value = pressedKey; const nextControl = inputControl.nextElementSibling; if (nextControl && nextControl.tagName === "INPUT") { nextControl.focus(); } collectValues.call(self); return; } } }); // Input event as a fallback: On successful input, the focus changes. element.addEventListener("input", function (event) { if (event.target.tagName !== "INPUT") return; if (event.target.value.length === 1) { const nextControl = event.target.nextElementSibling; if (nextControl && nextControl.tagName === "INPUT") { nextControl.focus(); } collectValues.call(self); } }); return this; } function collectValues() { const controlsValues = Array.from( this[digitsElementSymbol].querySelectorAll("input"), ).map((input) => input.value || " "); this.value = controlsValues.join(""); } /** * @private * @return {void} */ function initControlReferences() { this[digitsElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="digits"]`, ); } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <template id="digit"> <input maxlength="1" data-monster-attributes=" value path:digit.value"> </template> <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control"> <div part="digits" data-monster-role="digits" data-monster-insert="digit path:digitsControls" tabindex="-1"></div> </div>`; } registerCustomElement(Digits);