import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon'; import { loading } from './components.util'; import { _defer, reactivity, type Proxy, type Reactive } from './reactive'; export type RedrawableHTML = HTMLElement & { update?: (recursive: boolean) => void } export type Node = HTMLElement & { update?: (recursive: boolean) => void } | SVGElement | Text | undefined; export type NodeChildren = Array> | undefined; export type Class = string | Array | Record | undefined; type Listener = | ((this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any) | { options?: boolean | AddEventListenerOptions; listener: (this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any; } | undefined; export interface DOMList extends Array{ render(redraw?: boolean): void; }; export interface NodeProperties { attributes?: Record>; text?: Reactive; class?: Reactive; style?: Reactive | string>; listeners?: { [K in keyof HTMLElementEventMap]?: Listener }; } export const cancelPropagation = (e: Event) => e.stopImmediatePropagation(); export function dom(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T]; export function dom(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Reactive> }): HTMLElementTagNameMap[T] & { array?: DOMList }; export function dom(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node, list?: Reactive> }): HTMLElementTagNameMap[T] & { array?: DOMList } { const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList }; const _cache = new Map(); if(children) { if(Array.isArray(children)) { for(const c of children) { const child = typeof c === 'function' ? c() : c; child && element.appendChild(child); } } else if(children.list !== undefined) { reactivity(children.list, (list) => { element.replaceChildren(); list?.forEach(e => { let dom = _cache.get(e); if(!dom) { dom = children.render(e); _cache.set(e, dom); } dom && element.appendChild(dom); }); }) } } if(properties?.attributes) { for(const [k, v] of Object.entries(properties.attributes)) { reactivity(properties.attributes[k], (attribute) => { if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10)); else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute); }); } } if(properties?.text) { reactivity(properties.text, (text) => { if(typeof text === 'string') element.textContent = text; else if(typeof text === 'number') element.textContent = text.toString(); else element.appendChild(text as Text); }) } if(properties?.listeners) { 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); } } if(properties?.class) { reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes))); } if(properties?.style) { reactivity(properties.style, (style) => { if(typeof style === 'string') element.setAttribute('style', style); else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v); }) } return element; } export function div(cls?: Reactive, children?: NodeChildren): HTMLElementTagNameMap['div'] export function div(cls?: Reactive, children?: { render: (data: U) => Node, list?: Reactive> }): HTMLElementTagNameMap['div'] & { array: DOMList } export function div(cls?: Reactive, children?: NodeChildren | { render: (data: U) => Node, list?: Reactive> }): HTMLElementTagNameMap['div'] & { array?: DOMList } { //@ts-expect-error wtf is wrong here ??? return dom("div", { class: cls }, children); } export function span(cls?: Reactive, text?: Reactive): HTMLElementTagNameMap['span'] & { update?: (recursive: boolean) => void } { return dom("span", { class: cls, text: text }); } export function svg(tag: K, properties?: NodeProperties, children?: Array>): SVGElementTagNameMap[K] { const element = document.createElementNS("http://www.w3.org/2000/svg", tag); if(children) { for(const c of children) { const child = typeof c === 'function' ? c() : c; child && element.appendChild(child); } } if(properties?.attributes) { for(const [k, v] of Object.entries(properties.attributes)) { reactivity(properties.attributes[k], (attribute) => { if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10)); else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute); }); } } if(properties?.text) { reactivity(properties.text, (text) => { if(typeof text === 'string') element.textContent = text; else if(typeof text === 'number') element.textContent = text.toString(); else element.appendChild(text as Text); }) } if(properties?.class) { reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes))); } if(properties?.style) { reactivity(properties.style, (style) => { if(typeof style === 'string') element.setAttribute('style', style); else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v); }) } return element; } export function text(data: Reactive): Text { const text = document.createTextNode(''); reactivity(data, (txt) => text.textContent = txt.toString()); return text; } export interface IconProperties { mode?: 'svg' | 'style' | 'bg' | 'mask'; inline?: boolean; width?: string | number; height?: string | number; hFlip?: boolean; vFlip?: boolean; rotate?: number; style?: Reactive | string>; class?: Class; } const iconLoadingRegistry: Map> | null | undefined> = new Map(); export function icon(name: string, properties?: IconProperties) { const element = dom('div', { class: properties?.class, style: properties?.style }); const build = (icon: IconifyIcon | null | undefined) => { if(!icon) return element.replaceChildren(); const built = buildIcon(icon, properties); const dom = svg('svg', { attributes: built.attributes }); dom.innerHTML = built.body; element.replaceChildren(dom); } if(!iconLoaded(name)) { element.appendChild(loading('small')); if(!iconLoadingRegistry.has(name)) iconLoadingRegistry.set(name, loadIcon(name)); iconLoadingRegistry.get(name)?.then(build); } else build(getIcon(name)); return element; } export function mergeClasses(classes: Class): string { 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 ''; } }