ui/Input.tstypescript
import type { EventsHandler } from "../Events";
import { View } from "./View";
import type { IViewParams, ViewEventsList } from "./View";
import { stringTemplate, toFixedMax } from "../utils/shared";
interface IInputParams extends IViewParams {
label?: string;
value?: string;
min?: number;
max?: number;
type?: string;
step?: number | "any";
maxFixed?: number;
}
type InputEventsList = ["change"];
const INPUT_EVENTS: InputEventsList = ["change"];
const TEMPLATE = `<div class="og-input">
<div class="og-input-label">{label}</div>
<input type="{type}"/>
</div>`;
class Input extends View<null> {
public override events: EventsHandler<InputEventsList> & EventsHandler<ViewEventsList>;
protected _value: string;
protected _type: string;
protected _min?: number;
protected _max?: number;
protected _step: number | "any";
protected $label: HTMLElement | null;
protected $input: HTMLInputElement | null;
protected _maxFixed: number;
constructor(options: IInputParams = {}) {
super({
template: stringTemplate(TEMPLATE, {
label: options.label || "",
type: options.type || "text"
})
});
//@ts-ignore
this.events = this.events.registerNames(INPUT_EVENTS);
this._value = options.value || "";
this._type = options.type || "text";
this._min = options.min;
this._max = options.max;
this._step = options.step ?? "any";
this._maxFixed = options.maxFixed != undefined ? options.maxFixed : -1;
this.$label = null;
this.$input = null;
}
public override render(params: any): this {
super.render(params);
this.$label = this.select(".og-input-label")!;
if (this.$label.innerHTML === "") {
this.$label.style.display = "none";
}
this.$input = this.select<HTMLInputElement>("input");
if (this.$input && this._type === "number") {
this.$input.setAttribute("step", this._step.toString());
this.$input.setAttribute("inputmode", "decimal");
if (this._min !== undefined) {
this.$input.min = this._min.toString();
}
if (this._max !== undefined) {
this.$input.max = this._max.toString();
}
}
if (this.$input) {
this.$input.value = this._value;
}
this._initEvents();
return this;
}
protected _onResize = () => {};
public set value(val: string | number) {
this._setCommittedValue(this._formatValue(val));
}
protected _setCommittedValue(value: string, forceSync: boolean = false): void {
if (value !== this._value) {
this._value = value;
this._syncInputValue();
this.events.dispatch(this.events.change, this._value, this);
} else {
if (forceSync || document.activeElement !== this.$input) {
this._syncInputValue();
}
this.events.stopPropagation();
this.events.dispatch(this.events.change, this._value, this);
}
}
public get value(): string {
return this._value;
}
protected _formatValue(val: string | number): string {
return typeof val === "number" ? this._formatNumber(val) : val;
}
protected _formatNumber(val: number): string {
return toFixedMax(this._clampNumber(val), this._getMaxFixed());
}
protected _getMaxFixed(): number {
if (this._maxFixed >= 0 || typeof this._step !== "number") {
return this._maxFixed;
}
const stepStr = this._step.toString().toLowerCase();
if (stepStr.includes("e-")) {
return parseInt(stepStr.split("e-")[1]);
}
const decimalPart = stepStr.split(".")[1];
return decimalPart ? decimalPart.length : -1;
}
protected _clampNumber(value: number): number {
if (this._min !== undefined) {
value = Math.max(this._min, value);
}
if (this._max !== undefined) {
value = Math.min(this._max, value);
}
return value;
}
protected _parseNumber(value: string): number | null {
const normalized = value.trim().replace(",", ".");
if (normalized === "") return null;
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : null;
}
protected _normalizeUserValue(value: string): string | null {
if (this._type === "number") {
const parsed = this._parseNumber(value);
return parsed !== null ? this._formatNumber(parsed) : null;
}
return value;
}
protected _syncInputValue(): void {
if (this.$input) {
this.$input.value = this._value;
}
}
public override afterRender(parentNode: HTMLElement) {
this._initEvents();
}
protected _initEvents() {
//@ts-ignore
this.el?.addEventListener("mousewheel", this._onMouseWheel);
this.el?.addEventListener("wheel", this._onMouseWheelFF);
this.$input?.addEventListener("input", this._onInput);
this.$input?.addEventListener("change", this._onChange);
this.$input?.addEventListener("keydown", this._onKeyDown);
this.$input?.addEventListener("blur", this._onBlur);
}
protected _clearEvents() {
//@ts-ignore
this.el?.removeEventListener("mousewheel", this._onMouseWheel);
this.el?.removeEventListener("wheel", this._onMouseWheelFF);
this.$input?.removeEventListener("input", this._onInput);
this.$input?.removeEventListener("change", this._onChange);
this.$input?.removeEventListener("keydown", this._onKeyDown);
this.$input?.removeEventListener("blur", this._onBlur);
}
protected _onMouseWheel = (e: WheelEvent) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
e.stopPropagation();
};
protected _onMouseWheelFF = (e: WheelEvent) => {
this._onMouseWheel(e);
};
protected _onInput = (e: Event) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLInputElement;
let nextValue = target.value;
if (this._type === "number") {
nextValue = nextValue.replace(",", ".");
if (nextValue !== target.value) {
target.value = nextValue;
}
}
};
protected _onChange = (e: Event) => {
e.stopPropagation();
this._commitUserInput();
};
protected _onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
this._commitUserInput();
} else if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
this._syncInputValue();
} else if (this._type === "number" && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
e.preventDefault();
e.stopPropagation();
this._stepValue(e.key === "ArrowUp" ? 1 : -1);
}
};
protected _onBlur = () => {
this._commitUserInput();
};
protected _commitUserInput(): void {
if (!this.$input) return;
const value = this._normalizeUserValue(this.$input.value);
if (value === null || (this._type === "number" && this.$input.value.trim() === "")) {
this._syncInputValue();
return;
}
this._setCommittedValue(value, true);
}
protected _stepValue(direction: number): void {
if (!this.$input) return;
const step = typeof this._step === "number" ? this._step : 1;
const current = this._parseNumber(this.$input.value);
const committed = this._parseNumber(this._value);
const base = current ?? committed ?? this._min ?? 0;
this._setCommittedValue(this._formatNumber(base + direction * step), true);
}
public override remove() {
this._clearEvents();
super.remove();
}
set visibility(visibility: boolean) {
if (this.el) {
if (visibility) {
this.el.style.display = "";
} else {
this.el.style.display = "none";
}
}
}
}
export { Input };