/** * Setting the {@link UwcInputElement.type | `[type]`} to `"number"` will render * a number input. * * This input type only allows to enter numbers. One of the difference to the native * `` is that `` formats its displayed * value according to the nearest `[lang]` attribute consistently across browsers. * * Another is that accessing the {@link UwcInputElement.value | `.value`} will return a parsed * `number` already or `undefined` if the number cannot be parsed. * * Please keep in mind that _if_ you set {@link UwcInputElement.value | `.value`} to a `string` * (which is still allowed) that string MUST be formatted with a `.` (dot) as the * decimal separator and without grouping separators. * * @example * * ```html * * ``` * * @module */ import { localeContentAttribute__number } from "simple-custom-elements"; import { Connect, SupportedIdlType } from "../uwc-input"; import { iife, pipe } from "@hesxenon/prelude/Function.js"; import * as Either from "@hesxenon/prelude/Either.js"; import { Events } from "../events"; import { create, createEventRegistrar, dispatch, selectText, text, } from "@hesxenon/prelude/Dom.js"; import * as Root from "../parts/root"; /** * @internal */ export const connectNumber: Connect = (input, opts) => { const codec = localeContentAttribute__number(input.locale); const syncFormValues = () => { dispatch(input, { type: Events.formValuesChanged, detail: typeof input.value !== "number" ? [] : [String(input.value)], }); }; const validate = () => { dispatch(input, { type: Events.validityStateChanged, detail: { valueMissing: input.required && !input.value, rangeUnderflow: input.min != null && input.value != null && input.value < input.min, rangeOverflow: input.max != null && input.value != null && input.value > input.max, }, }); }; const toString = (idl: SupportedIdlType) => pipe( codec.encode.call(input, idl as number | undefined), Either.getOrElse(() => ""), ); const cache = iife(() => { const idl = input.value as number | undefined; return { idl, cursorRange: { start: 0, end: 0 }, textContent: toString(idl), }; }); const on = createEventRegistrar(input, opts.disconnect); const initial = input.value; let snapshot = input.value; on("focusin", () => { snapshot = input.value; }); on("focusout", () => { if (snapshot !== input.value) { dispatch(input, { type: "change" }); } }); on("click", (e) => { if (!e.isTrusted) { // TODO: workaround for https://github.com/testing-library/user-event/issues/1237 e.preventDefault(); } content.focus(); }); on(Events.valueChanged, () => { if (input.value !== cache.idl) { content.textContent = toString(input.value); } syncFormValues(); }); on(Events.clear, () => { input.value = undefined; }); on([Events.nameChanged, Events.formAssociated], syncFormValues); on( [Events.formValuesChanged, Events.minChanged, Events.maxChanged], validate, ); on(Events.formReset, () => { input.value = initial; }); const content = create( "div", { contentEditable: "true", oninput: (e) => { const before = input.value as number | undefined; pipe( codec.decode(content.textContent), Either.match( () => { content.textContent = cache.textContent; selectText( document.activeElement!.firstChild ?? document.activeElement!, cache.cursorRange.start, cache.cursorRange.end, ); }, (value) => { cache.textContent = !content.innerText.includes("\n") ? content.innerText : (content.innerText = content.innerText.replaceAll("\n", "")); input.value = cache.idl = value; const curentRange = getSelection()?.getRangeAt(0); cache.cursorRange.start = curentRange?.startOffset ?? 0; cache.cursorRange.end = curentRange?.endOffset ?? cache.cursorRange.start; }, ), ); if (input.value === before) { e.stopPropagation(); } }, }, [text(cache.textContent)], ); const placeholder = create("div", { part: "placeholder" }, [ text(input.getAttribute("placeholder") ?? ""), ]); Root.replaceChildren(input, [content, placeholder]); syncFormValues(); };