UNPKG

typed-dom

Version:

A value-level and type-level DOM builder.

256 lines (239 loc) 9.72 kB
import { hasOwnProperty } from 'spica/alias'; declare global { interface ShadowHostHTMLElementTagNameMap { 'article': HTMLElement; 'aside': HTMLElement; 'blockquote': HTMLQuoteElement; 'body': HTMLBodyElement; 'div': HTMLDivElement; 'footer': HTMLElement; 'h1': HTMLHeadingElement; 'h2': HTMLHeadingElement; 'h3': HTMLHeadingElement; 'h4': HTMLHeadingElement; 'h5': HTMLHeadingElement; 'h6': HTMLHeadingElement; 'header': HTMLElement; 'main': HTMLElement; 'nav': HTMLElement; 'p': HTMLParagraphElement; 'section': HTMLElement; 'span': HTMLSpanElement; } interface HTMLElementTagNameMap extends ShadowHostHTMLElementTagNameMap { } } export const enum NS { HTML = 'HTML', SVG = 'SVG', Math = 'MathML', } // {HTML,SVG,}ElementEventMapを使用しないがEvent型しか使われてないので問題ない interface NodeEvent<N extends Node> extends Event { readonly target: Node; readonly currentTarget: N; } type NodeEventListener<N extends Node> = (ev: NodeEvent<N>) => void; export type TagNameMap = object; export type Attrs<E extends Element = Element> = Record<string, string | NodeEventListener<E> | null | undefined>; export type Children = Iterable<string | Node> | string | undefined; export interface Factory<M extends TagNameMap> { <T extends keyof M & string>(tag: T, children?: Children): M[T]; <T extends keyof M & string>(tag: T, attrs: Attrs<Extract<M[T], Element>> | undefined, children?: Children): M[T]; } export function shadow<M extends ShadowHostHTMLElementTagNameMap>(el: keyof M & string | Extract<M[keyof M & string], Element>, factory?: Factory<M>): ShadowRoot; export function shadow<M extends ShadowHostHTMLElementTagNameMap>(el: keyof M & string | Extract<M[keyof M & string], Element>, children?: Children, factory?: Factory<M>): ShadowRoot; export function shadow<M extends ShadowHostHTMLElementTagNameMap>(el: keyof M & string | Extract<M[keyof M & string], Element>, opts?: ShadowRootInit, factory?: Factory<M>): ShadowRoot; export function shadow<M extends ShadowHostHTMLElementTagNameMap>(el: keyof M & string | Extract<M[keyof M & string], Element>, opts?: ShadowRootInit, children?: Children, factory?: Factory<M>): ShadowRoot; export function shadow<M extends ShadowHostHTMLElementTagNameMap>(el: keyof M & string | Extract<M[keyof M & string], Element>, opts?: ShadowRootInit | Factory<M> | Children, children?: Factory<M> | Children, factory: Factory<M> = html as unknown as Factory<M>): ShadowRoot { if (typeof el === 'string') return shadow(factory(el) as Extract<M[keyof M & string], Element>, opts as ShadowRootInit, children as Children, factory); if (typeof opts === 'function') return shadow(el, undefined, children as Children, opts); if (typeof children === 'function') return shadow(el, opts as ShadowRootInit, undefined, children); if (isChildren(opts)) return shadow(el, undefined, opts, factory); return defineChildren( opts ? el.attachShadow(opts) : el.shadowRoot ?? el.attachShadow({ mode: 'open' }), children); } export function frag(children?: Children): DocumentFragment { return defineChildren(document.createDocumentFragment(), children); } export const html = element<HTMLElementTagNameMap>(document, NS.HTML); export const svg = element<SVGElementTagNameMap>(document, NS.SVG); export const math = element<MathMLElementTagNameMap>(document, NS.Math); export function text(source: string): Text { return document.createTextNode(source); } export function element<M extends HTMLElementTagNameMap>(context: Document | ShadowRoot, ns: NS.HTML): Factory<M>; export function element<M extends SVGElementTagNameMap>(context: Document | ShadowRoot, ns: NS.SVG): Factory<M>; export function element<M extends MathMLElementTagNameMap>(context: Document | ShadowRoot, ns: NS.Math): Factory<M>; export function element<M extends TagNameMap>(context: Document | ShadowRoot, ns: NS): Factory<M> { if (context instanceof ShadowRoot) { const root = context; context = document.implementation.createHTMLDocument(); // @ts-ignore root.customElementRegistry.initialize(context); } return (tag: string, attrs?: Attrs | Children, children?: Children) => { return !attrs || isChildren(attrs) ? defineChildren(elem(context, ns, tag, {}), attrs ?? children) : defineChildren(defineAttrs(elem(context, ns, tag, attrs), attrs), children); }; } function elem(context: Document, ns: NS, tag: string, attrs: Attrs): Element { const opts = { is: attrs['is'], customElementRegistry: attrs['customElementRegistry'], } as ElementCreationOptions; switch (ns) { case NS.HTML: return context.createElement(tag, opts); case NS.SVG: return context.createElementNS('http://www.w3.org/2000/svg', tag, opts); case NS.Math: return context.createElementNS('http://www.w3.org/1998/Math/MathML', tag, opts); } } export function define<E extends Element>(el: E, attrs?: Attrs<E>, children?: Children): E; export function define<E extends Element | DocumentFragment | ShadowRoot>(node: E, children?: Children): E; export function define<E extends Element | DocumentFragment | ShadowRoot>(node: E, attrs?: Attrs | Children, children?: Children): E { // Bug: TypeScript // Need the next type assertions to suppress an impossible type error on dependent projects. // Probably caused by typed-query-selector. // // typed-dom/dom.ts(113,3): Error TS2322: Type 'ParentNode & Node' is not assignable to type 'E'. // 'E' could be instantiated with an arbitrary type which could be unrelated to 'ParentNode & Node'. // return !attrs || isChildren(attrs) ? defineChildren(node, attrs ?? children) as E : defineChildren(defineAttrs(node as Element, attrs), children) as E; } function defineAttrs<E extends Element>(el: E, attrs: Attrs): E { for (const name of Object.keys(attrs)) { switch (name) { case 'is': case 'customElementRegistry': continue; } const value = attrs[name]; switch (typeof value) { case 'string': el.setAttribute(name, value); if (name.startsWith('on')) { const type = name.slice(2).toLowerCase(); switch (type) { case 'mutate': case 'connect': case 'disconnect': const prop = `on${type}`; el[prop] ?? Object.defineProperty(el, prop, { configurable: true, enumerable: false, writable: true, value: prop in el && !hasOwnProperty(el, prop) ? (ev: Event) => ev.returnValue : '', }); assert.deepStrictEqual({ ...el }, {}); } } continue; case 'function': if (name.length < 3) throw new Error(`Typed-DOM: Attribute names for event listeners must have an event name but got "${name}"`); const names = name.split(/\s+/); for (const name of names) { if (!name.startsWith('on')) throw new Error(`Typed-DOM: Attribute names for event listeners must start with "on" but got "${name}"`); const type = name.slice(2).toLowerCase(); el.addEventListener(type, value, { passive: [ 'wheel', 'mousewheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel', ].includes(type), }); switch (type) { case 'mutate': case 'connect': case 'disconnect': const prop = `on${type}`; el[prop] ?? Object.defineProperty(el, prop, { configurable: true, enumerable: false, writable: true, value: prop in el && !hasOwnProperty(el, prop) ? (ev: Event) => ev.returnValue : '', }); assert.deepStrictEqual({ ...el }, {}); } } continue; case 'object': assert(value === null); el.removeAttribute(name); continue; default: continue; } } return el; } function defineChildren<N extends ParentNode & Node>(node: N, children: Children): N { if (children === undefined) return node; if (typeof children === 'string') { node.textContent = children; } else { node.replaceChildren(...children); } return node; } export function isChildren(value: Attrs | Children | ShadowRootInit): value is NonNullable<Children> { return value?.[Symbol.iterator] !== undefined; } export function append<N extends ParentNode & Node>(node: N, children: Children): N { if (children === undefined) return node; if (typeof children === 'string') { node.append(children); } else { for (const child of children) { typeof child === 'object' ? node.appendChild(child) : node.append(child); } } return node; } export function prepend<N extends ParentNode & Node>(node: N, children: Children): N { if (children === undefined) return node; if (typeof children === 'string') { node.prepend(children); } else { for (const child of children) { typeof child === 'object' ? node.insertBefore(child, null) : node.prepend(child); } } return node; } export function* defrag<N extends string | Node>(nodes: Iterable<N>): Iterable<string | N> { let acc = ''; for (const node of nodes) { if (typeof node === 'string') { acc += node; } else { if (acc) yield acc; acc = ''; yield node; } } if (acc) yield acc; }