You've already forked obsidian-visualiser
Change shared files naming. Rework tree structure and item management rendering.
This commit is contained in:
268
shared/dom.ts
Normal file
268
shared/dom.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
||||
import { loading } from './components';
|
||||
import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive';
|
||||
|
||||
export type RedrawableHTML = HTMLElement;
|
||||
export type Node = HTMLElement | SVGElement | Text | undefined;
|
||||
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
||||
|
||||
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
||||
type Listener<K extends keyof HTMLElementEventMap> = | ((this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any) | {
|
||||
options?: boolean | AddEventListenerOptions;
|
||||
listener: (this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any;
|
||||
} | undefined;
|
||||
|
||||
export interface DOMList<T> extends Array<T>{
|
||||
render(redraw?: boolean): void;
|
||||
};
|
||||
|
||||
export interface NodeProperties
|
||||
{
|
||||
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
|
||||
text?: Reactive<string | number | Text>;
|
||||
class?: Reactive<Class>;
|
||||
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
|
||||
listeners?: {
|
||||
[K in keyof HTMLElementEventMap]?: Listener<K>
|
||||
};
|
||||
}
|
||||
|
||||
function append(dom: Element, children: Node | Node[] | undefined)
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
children.forEach(e => e && dom.appendChild(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
children && dom.appendChild(children);
|
||||
}
|
||||
}
|
||||
|
||||
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T];
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
{
|
||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
const _cache = new Map<U, Node | Node[] | undefined>();
|
||||
|
||||
if(children)
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
reactivity(children, (_children) => {
|
||||
element.replaceChildren();
|
||||
for(const c of _children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(children.list !== undefined)
|
||||
{
|
||||
let fallback: Node | Node[] | undefined;
|
||||
children.fallback && reactivity(children.fallback, (_fallback) => {
|
||||
if(_fallback)
|
||||
{
|
||||
if(Array.isArray(fallback) && fallback.filter(e => !!e)[0] && fallback.filter(e => !!e)[0]!.parentElement === element)
|
||||
element.replaceChildren(), append(element, _fallback);
|
||||
else if(!Array.isArray(fallback) && fallback?.parentElement === element)
|
||||
element.replaceChildren(), append(element, _fallback);
|
||||
}
|
||||
fallback = _fallback;
|
||||
});
|
||||
reactivity(children.list, (list) => {
|
||||
element.replaceChildren();
|
||||
if(list.length === 0)
|
||||
{
|
||||
fallback ??= children?.fallback ? children.fallback() : undefined;
|
||||
append(element, fallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.forEach(e => {
|
||||
const child = raw(children.render(e, _cache.get(e)));
|
||||
_cache.set(e, child);
|
||||
append(element, 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?.listeners)
|
||||
{
|
||||
for(let [k, v] of Object.entries(properties.listeners))
|
||||
{
|
||||
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
|
||||
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<Class>, children?: NodeChildren): HTMLElementTagNameMap['div']
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
|
||||
{
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span']
|
||||
{
|
||||
return dom("span", { class: cls, text: text });
|
||||
}
|
||||
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Array<Reactive<SVGElement>>): 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<string | number>): 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<Record<string, string | undefined> | string>;
|
||||
class?: Class;
|
||||
}
|
||||
const iconLoadingRegistry: Map<string, Promise<Required<IconifyIcon>> | null | undefined> = new Map();
|
||||
export function icon(name: Reactive<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);
|
||||
}
|
||||
reactivity(name, (name) => {
|
||||
if(!iconLoaded(name))
|
||||
{
|
||||
element.replaceChildren(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 '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user