182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
import { iconLoaded, loadIcon } from 'iconify-icon';
|
|
|
|
export type Node = HTMLElement | SVGElement | Text | undefined;
|
|
export type NodeChildren = Array<Node> | undefined;
|
|
|
|
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
|
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
|
|
options?: boolean | AddEventListenerOptions;
|
|
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
|
|
} | undefined;
|
|
|
|
export interface NodeProperties
|
|
{
|
|
attributes?: Record<string, string | undefined | boolean | number>;
|
|
text?: string;
|
|
class?: Class;
|
|
style?: Record<string, string | undefined | boolean | number> | string;
|
|
listeners?: {
|
|
[K in keyof HTMLElementEventMap]?: Listener<K>
|
|
};
|
|
}
|
|
|
|
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
|
|
export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[K]
|
|
{
|
|
const element = document.createElement(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' || typeof v === 'number') element.setAttribute(k, v.toString(10));
|
|
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
|
|
|
|
if(properties?.text)
|
|
element.textContent = properties.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);
|
|
}
|
|
}
|
|
|
|
styling(element, properties ?? {});
|
|
|
|
return element;
|
|
}
|
|
export function div(cls?: Class, children?: NodeChildren): HTMLDivElement
|
|
{
|
|
return dom("div", { class: cls }, children);
|
|
}
|
|
export function span(cls?: Class, text?: string): HTMLSpanElement
|
|
{
|
|
return dom("span", { class: cls, text: text });
|
|
}
|
|
export function svg<K extends keyof SVGElementTagNameMap>(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)
|
|
element.textContent = properties.text;
|
|
|
|
styling(element, properties ?? {});
|
|
|
|
return element;
|
|
}
|
|
export function text(data: string): Text
|
|
{
|
|
return document.createTextNode(data);
|
|
}
|
|
export function styling(element: SVGElement | HTMLElement, properties: {
|
|
class?: Class;
|
|
style?: Record<string, string | undefined | boolean | number> | 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, string | undefined> | string;
|
|
class?: Class;
|
|
}
|
|
const iconCache: Map<string, HTMLElement> = 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(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 '';
|
|
}
|
|
} |