import { iconLoaded, loadIcon } from 'iconify-icon'; export type RedrawableHTML = HTMLElementTagNameMap[T] & { update: (recursive: boolean) => void } export type Node = RedrawableHTML & { update: (recursive: boolean) => void } | SVGElement | Text | undefined; export type NodeChildren = Array | undefined; export type Class = Reactive | Record | undefined>; type Listener = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | { options?: boolean | AddEventListenerOptions; listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any; } | undefined; export type Reactive = T | (() => T); export interface DOMList extends Array{ render(redraw?: boolean): void; }; export interface NodeProperties { attributes?: Record>; text?: Reactive; class?: Class; style?: Reactive | string>; listeners?: { [K in keyof HTMLElementEventMap]?: Listener }; } let defered = false, _set = new Set<() => void>(); const _defer = (fn: () => void) => { if(!defered) { defered = true; queueMicrotask(() => { _set.forEach(e => e()); _set.clear(); defered = false; }); } _set.add(fn); } export const cancelPropagation = (e: Event) => e.stopImmediatePropagation(); export function dom(tag: T, properties?: NodeProperties, children?: NodeChildren): RedrawableHTML; export function dom(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Array, redraw?: boolean }): RedrawableHTML & { array?: DOMList }; export function dom(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node, list?: Array, redraw?: boolean }): RedrawableHTML & { array?: DOMList } { const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList, update: (recursive: boolean) => void }; let setup = true, updating = false; const _cache = new Map(); const update = (recursive: boolean) => { updating = true; if(children !== undefined && (setup || recursive)) { if(Array.isArray(children)) { for(const c of children) { if(c !== undefined) { element.appendChild(c); recursive && 'update' in c && _defer(() => c.update(true)); } } } else if(children.list !== undefined) { if(setup) { children.list.forEach(e => _cache.set(e, children.render(e))); const _push = children.list.push; children.list.push = (...items: U[]) => { items.forEach(e => { if(!_cache.has(e)) _cache.set(e, element.appendChild(children.render(e))); else element.appendChild(_cache.get(e)); }); if(children.redraw) _defer(() => update(false)); return _push.bind(children.list)(...items); }; const _splice = children.list.splice; children.list.splice = (start: number, deleteCount: number, ...items: U[]) => { const list = _splice.bind(children.list)(start, deleteCount, ...items); list.forEach(e => { if(!children.list!.find(_e => _e === e)) _cache.delete(e); }); element.array!.render(); return list; }; } else if(recursive) _cache.forEach((v, k) => v && 'update' in v && v.update(true)); element.array = children.list as DOMList; element.array.render = (redraw?: boolean) => { element.replaceChildren(...children.list?.map(e => _cache.get(e)).filter(e => !!e) ?? []); if((redraw !== undefined || children.redraw !== undefined) && !updating) _defer(() => update(redraw ?? children.redraw!)); } element.array.render(); } } if(properties?.attributes) { for(const [k, v] of Object.entries(properties.attributes)) { if(!setup && typeof v !== 'function') continue; const value = typeof v === 'function' ? v() : v; if(typeof value === 'string' || typeof value === 'number') element.setAttribute(k, value.toString(10)); else if(typeof value === 'boolean') element.toggleAttribute(k, value); } } if(properties?.text && (setup || typeof properties.text === 'function')) { const text = typeof properties.text === 'function' ? properties.text() : properties.text; if(typeof text === 'string') element.textContent = text; else element.appendChild(text as Text); } if(properties?.listeners && setup) { for(let [k, v] of Object.entries(properties.listeners)) { const key = k as keyof HTMLElementEventMap, value = v as Listener; if(typeof value === 'function') element.addEventListener(key, value.bind(element)); else if(value) element.addEventListener(key, value.listener.bind(element), value.options); } } styling(element, properties ?? {}); updating = false; }; update(false); setup = false; element.update = update; return element; } export function div(cls?: Class, children?: NodeChildren): RedrawableHTML<'div'> export function div(cls?: Class, children?: { render: (data: U) => Node, list?: Array, redraw?: boolean }): RedrawableHTML<'div'> & { array: DOMList } export function div(cls?: Class, children?: NodeChildren | { render: (data: U) => Node, list?: Array, redraw?: boolean }): RedrawableHTML<'div'> & { array?: DOMList } { //@ts-expect-error return dom("div", { class: cls }, children); } export function span(cls?: Class, text?: Reactive): RedrawableHTML<'span'> { return dom("span", { class: cls, text: text }); } export function svg(tag: K, properties?: NodeProperties, children?: SVGElement[]): SVGElementTagNameMap[K] { const element = document.createElementNS("http://www.w3.org/2000/svg", tag); if(children && children.length > 0) for(const c of children) if(c !== undefined) element.appendChild(c); if(properties?.attributes) for(const [k, v] of Object.entries(properties.attributes)) if(typeof v === 'string') element.setAttribute(k, v); else if(typeof v === 'boolean') element.toggleAttribute(k, v); if(properties?.text && typeof properties.text === 'string') element.textContent = properties.text; styling(element, properties ?? {}); return element; } export function text(data: string): Text; export function text(data: {}, _txt: Reactive): Text; export function text(data: any, _txt?: Reactive): Text { if(typeof data === 'string') return document.createTextNode(data); else if(_txt) { const cache = new Map(); let txtCache = (typeof _txt === 'function' ? _txt() : _txt); const setup = (property: string) => { const prop = property.split('.'); let obj = data; for(let i = 0; i < prop.length - 1; i++) { if(prop[i]! in obj) obj = obj[prop[i]!]; else return 0; } const last = prop.slice(-1)[0]!; if(last in obj) { const prototype = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), last); let clone = obj[last]; delete obj[last]; Object.defineProperty(obj, last, { ...prototype, get: () => prototype?.get ? prototype.get() : clone, set: (v) => { if(prototype?.set) { prototype.set(v); clone = obj[last]; } else if(!prototype?.get) { clone = v; } cache.set(property, v); replace(); }, enumerable: true, configurable: true, }); cache.set(property, clone); return obj[last]; } else return 0; } const apply = (_setup: boolean) => txtCache.replace(/\{\{(.+?)\}\}/g, (_, txt: string) => { let i = 0, current = 0, property = '', nextOp = ''; const _compute = () => { if(property.length > 0) { let value = 0; if(_setup) value = setup(property); else value = cache.get(property)!; if(nextOp === '+') current += value; else if(nextOp === '-') current -= value; else if(nextOp === '*') current *= value; else if(nextOp === '/') current /= value; else if(nextOp === '%') current /= value; else if(nextOp === '') current = value; nextOp = ''; property = ''; } } while(i < txt.length) { switch(txt.charAt(i)) { case '+': case '-': case '*': case '/': case '%': _compute(); nextOp = txt.charAt(i).trim(); break; case ' ': break; default: property += txt.charAt(i); break; } i++; } _compute(); return current.toString(); }); const replace = () => { const txt = (typeof _txt === 'function' ? _txt() : _txt); if(txt !== txtCache) { txtCache = txt; node.textContent = apply(true); } else node.textContent = apply(false); }; const node = document.createTextNode(apply(true)); return node; } else return document.createTextNode(''); } export function styling(element: SVGElement | RedrawableHTML, properties: { class?: Class; style?: Reactive | string>; }): SVGElement | HTMLElement { if(properties?.class) { element.setAttribute('class', mergeClasses(properties.class)); } if(properties?.style) { if(typeof properties.style === 'string') { element.setAttribute('style', properties.style); } else for(const [k, v] of Object.entries(properties.style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v); } return element; } export interface IconProperties { mode?: string; inline?: boolean; noobserver?: boolean; width?: string | number; height?: string | number; flip?: string; rotate?: number|string; style?: Record | string; class?: Class; } const iconCache: Map = new Map(); export function icon(name: string, properties?: IconProperties): HTMLElement { let element; if(iconCache.has(name)) element = iconCache.get(name)!.cloneNode() as HTMLElement; else { element = document.createElement('iconify-icon'); if(!iconLoaded(name)) loadIcon(name); element.setAttribute('icon', name); iconCache.set(name, element.cloneNode() as HTMLElement); } properties?.mode && element.setAttribute('mode', properties?.mode.toString()); properties?.inline && element.toggleAttribute('inline', properties?.inline); element.toggleAttribute('noobserver', properties?.noobserver ?? true); properties?.width && element.setAttribute('width', properties?.width.toString()); properties?.height && element.setAttribute('height', properties?.height.toString()); properties?.flip && element.setAttribute('flip', properties?.flip.toString()); properties?.rotate && element.setAttribute('rotate', properties?.rotate.toString()); if(properties?.class) { element.setAttribute('class', mergeClasses(properties.class)); } if(properties?.style) { if(typeof properties.style === 'string') { element.setAttribute('style', properties.style); } else for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) element.attributeStyleMap.set(k, v); } return element; } export function mergeClasses(cls: Class): string { const classes = typeof cls === 'function' ? cls() : cls; if(typeof classes === 'string') { return classes.trim(); } else if(Array.isArray(classes)) { return classes.map(e => mergeClasses(e)).join(' '); } else if(classes) { return Object.entries(classes).filter(e => e[1]).map(e => e[0].trim()).join(' '); } else { return ''; } }