ui/Color.tstypescript
import type { EventsHandler } from "../Events";
import { View } from "./View";
import type { IViewParams, ViewEventsList } from "./View";
import { stringTemplate } from "../utils/shared";

interface IColorParams extends IViewParams {
    label?: string;
    value?: string;
}

type ColorEventsList = ["input"];

const COLOR_EVENTS: ColorEventsList = ["input"];

const TEMPLATE = `<div class="og-color">
      <label for="{id}" class="og-color-label">{label}</label>
      <div class="og-color-controls">
        <input class="og-color-picker" type="color" id="{id}" name="{id}" value="{value}"/>
        <input class="og-color-value" type="text" value="{value}" placeholder="#ffffff"/>
      </div>
    </div>`;

let __labelCounter__ = 0;

class Color extends View<null> {
    public override events: EventsHandler<ColorEventsList> & EventsHandler<ViewEventsList>;

    protected _value: string;
    protected $label: HTMLElement | null;
    protected $pickerInput: HTMLInputElement | null;
    protected $valueInput: HTMLInputElement | null;

    constructor(options: IColorParams = {}) {
        const value = Color.normalizeColor(options.value) || "#000000";

        super({
            template: stringTemplate(TEMPLATE, {
                id: `color-${__labelCounter__++}`,
                label: options.label || "",
                value
            })
        });

        //@ts-ignore
        this.events = this.events.registerNames(COLOR_EVENTS);

        this._value = value;

        this.$label = null;
        this.$pickerInput = null;
        this.$valueInput = null;
    }

    static normalizeColor(color: string | undefined): string | null {
        if (!color) return null;

        let value = color.trim();
        const shortMatch = value.match(/^#([0-9a-fA-F]{3})$/);
        if (shortMatch) {
            value = `#${shortMatch[1]
                .split("")
                .map((ch) => ch + ch)
                .join("")}`;
        }

        return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : null;
    }

    public override render(params: any): this {
        super.render(params);

        this.$label = this.select(".og-color-label")!;
        if (this.$label.innerHTML === "") {
            this.$label.style.display = "none";
        }
        this.$pickerInput = this.select<HTMLInputElement>(".og-color-picker");
        this.$valueInput = this.select<HTMLInputElement>(".og-color-value");

        this._initEvents();

        return this;
    }

    public set value(val: string) {
        const color = Color.normalizeColor(val);
        if (color && color !== this._value) {
            this._value = color;
            this.$pickerInput!.value = this._value;
            this.$valueInput!.value = this._value;
            this.events.dispatch(this.events.input, this._value, this);
        }
    }

    public get value(): string {
        return this._value;
    }

    public override afterRender(parentNode: HTMLElement) {
        this._initEvents();
    }

    protected _initEvents() {
        this.$pickerInput?.addEventListener("input", this._onPickerInput);
        this.$valueInput?.addEventListener("input", this._onValueInput);
    }

    protected _clearEvents() {
        this.$pickerInput?.removeEventListener("input", this._onPickerInput);
        this.$valueInput?.removeEventListener("input", this._onValueInput);
    }

    protected _onPickerInput = (e: Event) => {
        this.value = (e.target as HTMLInputElement).value;
    };

    protected _onValueInput = (e: Event) => {
        const color = Color.normalizeColor((e.target as HTMLInputElement).value);
        if (color) {
            this.value = color;
        }
    };

    public override remove() {
        this._clearEvents();
        super.remove();
    }
}

export { Color };