Skip to content
Snippets Groups Projects
Verified Commit 3f6eb292 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: finalize digit control #300

parent 110e8665
No related branches found
No related tags found
No related merge requests found
/**
* 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];
}
}
/**
* @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);
@import "../../style/color.pcss";
@import "../../style/theme.pcss";
@import "../../style/border.pcss";
@import "../../style/form.pcss";
@import "../../style/control.pcss";
@import "../../style/badge.pcss";
@import '../../style/mixin/typography.pcss';
@import '../../style/mixin/hover.pcss';
@import "../../style/control.pcss";
@import "../../style/floating-ui.pcss";
@import "./invalid.pcss";
[data-monster-role="control"] {
@mixin text;
}
[data-monster-role="digits"] {
display: grid;
grid-template-columns: 1fr;
grid-gap: 0.3rem;
accent-color: var(--monster-color-secondary-2);
}
[data-monster-role="digits"] > input {
width: 2rem;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.invalid {
outline: 1px solid var(--monster-color-error-2) !important;
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment