/**
* 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();
};