export interface Type extends Function { new (...args: any[]): T; } export abstract class ValueElement extends HTMLElement { static observedAttributes = ["value"]; private _value: T | null = null; get value(): T | null { return this._value; } set value(value: T | null) { if (value) { this.setAttribute("value", JSON.stringify(value)); } else if (this.hasAttribute("value")) { this.removeAttribute("value"); } } options: O | null; constructor(value: T | null = null, options: O | null = null) { super(); if (value) { this.value = value; } this.options = options; this.onInit(); } onInit() {} beforeRefresh() {} onRefresh() {} abstract buildHTML(): string; refresh() { const rawValue = this.getAttribute("value"); if (rawValue) { this._value = JSON.parse(rawValue); } else { this._value = null; } if (this.className) { this.classList = this.className; } this.beforeRefresh(); this.innerHTML = this.buildHTML(); this.onRefresh(); } connectedCallback() { this.refresh(); } attributeChangedCallback(name: string, oldValue: any, newValue: any) { setTimeout(() => this.refresh()); } } export interface ListOptions { onClick?: (element: ListItemElement) => void; onAdd?: (element: ListItemElement) => void; onRemove?: (element: ListItemElement) => void; } export abstract class ListItemElement extends ValueElement> { abstract getHref(value: T): string; abstract getContent(value: T): string; className = "list-group-item list-group-item-action"; onInit(): void { this.handleClick = this.handleClick.bind(this); this.handleRemoveClick = this.handleRemoveClick.bind(this); this.handleAddClick = this.handleAddClick.bind(this); } buildHTML(): string { if (!this.value) { return ""; } const href = this.getHref(this.value); const content = this.getContent(this.value); return ` + ${content} ✕ `; } onRefresh(): void { this.addEventListener("click", this.handleClick); if (this.options?.onAdd) { this.querySelector("[data-add]")?.addEventListener("click", this.handleAddClick); } else { this.querySelector("[data-add]")?.remove(); } if (this.options?.onRemove) { this.querySelector("[data-remove]")?.addEventListener("click", this.handleRemoveClick); } else { this.querySelector("[data-remove]")?.remove(); } } handleClick(event: MouseEvent) { if (this.value && this.options?.onClick) { this.options.onClick(this); } } handleAddClick(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); if (this.value && this.options?.onAdd) { this.options.onAdd(this); } } handleRemoveClick(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); if (this.value && this.options?.onRemove) { this.options.onRemove(this); } } disconnectedCallback() { this.removeEventListener("click", this.handleClick); this.querySelector("[data-add]")?.removeEventListener("click", this.handleAddClick); this.querySelector("[data-remove]")?.removeEventListener("click", this.handleRemoveClick); } } export abstract class ListElement = ListItemElement> extends ValueElement< T[], ListOptions > { static observedAttributes = ["value", "onadd", "onremove", "onclick"]; abstract childType: Type; className = "list-group"; buildHTML(): string { return ""; } beforeRefresh() { if (this.hasAttribute("onclick")) { const onclick = new Function("element", this.getAttribute("onclick")!) as ListOptions["onClick"]; if (!this.options) { this.options = {}; } this.options.onClick = onclick; } if (this.hasAttribute("onadd")) { const onadd = new Function("element", this.getAttribute("onadd")!) as ListOptions["onAdd"]; if (!this.options) { this.options = {}; } this.options.onAdd = onadd; } if (this.hasAttribute("onremove")) { const onremove = new Function("element", this.getAttribute("onremove")!) as ListOptions["onRemove"]; if (!this.options) { this.options = {}; } this.options.onRemove = onremove; } } onRefresh() { if (this.value) { for (const item of this.value) { this.appendChild(new this.childType(item, this.options)); } } } }