UNPKG

@thi.ng/rdom

Version:

Lightweight, reactive, VDOM-less UI/DOM components with async lifecycle and @thi.ng/hiccup compatible

234 lines (233 loc) 7.22 kB
import { deref, isDeref } from "@thi.ng/api/deref"; import { implementsFunction } from "@thi.ng/checks/implements-function"; import { isArray } from "@thi.ng/checks/is-array"; import { isFunction } from "@thi.ng/checks/is-function"; import { isNotStringAndIterable } from "@thi.ng/checks/is-not-string-iterable"; import { isNumber } from "@thi.ng/checks/is-number"; import { isString } from "@thi.ng/checks/is-string"; import { assert } from "@thi.ng/errors/assert"; import { unsupported } from "@thi.ng/errors/unsupported"; import { ATTRIB_JOIN_DELIMS, NO_SPANS, RE_TAG, SVG_TAGS } from "@thi.ng/hiccup/api"; import { mergeClasses, mergeEmmetAttribs } from "@thi.ng/hiccup/attribs"; import { formatPrefixes } from "@thi.ng/hiccup/prefix"; import { XML_SVG, XML_XLINK, XML_XMLNS } from "@thi.ng/prefixes/xml"; import { isComment, isComponent } from "./checks.js"; const $tree = async (tree, parent, idx = -1) => isArray(tree) ? isComment(tree) ? $comment(tree.slice(1), parent, idx) : $treeElem(tree, parent, idx) : isComponent(tree) ? tree.mount(parent, idx) : isDeref(tree) ? $tree(tree.deref(), parent) : isFunction(tree) ? $tree(tree(), parent, idx) : isNotStringAndIterable(tree) ? $treeIter(tree, parent) : tree != null ? $el("span", null, tree, parent, idx) : null; const $treeElem = (tree, parent, idx) => { const tag = tree[0]; return isString(tag) ? $treeTag(tree, parent, idx) : ( // [icomponent, ...args] isComponent(tag) ? tag.mount(parent, idx, ...tree.slice(1)) : ( // [fn, ...args] isFunction(tag) ? $tree(tag.apply(null, tree.slice(1)), parent) : ( // unsupported unsupported(`tag: ${tag}`) ) ) ); }; const $treeTag = (tree, parent, idx) => { const n = tree.length; const { 0: tag, 1: attribs, 2: body } = tree; if (n === 3 && (isString(body) || isNumber(body))) { const tmp = /^\w+/.exec(tag); if (tmp && NO_SPANS[tmp[0]]) { return $el(tag, attribs, body, parent, idx); } } parent = $el(tag, attribs, null, parent, idx); for (let i = 2; i < n; i++) { $tree(tree[i], parent); } return parent; }; const $treeIter = (tree, parent) => { for (let t of tree) { $tree(t, parent); } return null; }; const $el = (tag, attribs, body, parent, idx = -1) => { const match = RE_TAG.exec(tag); if (match) { attribs = mergeEmmetAttribs( attribs ? { ...attribs } : {}, match[2], match[3] ); tag = match[1]; } let el; const qidx = tag.indexOf(":"); if (qidx < 0) { el = SVG_TAGS[tag] ? document.createElementNS(XML_SVG, tag) : document.createElement(tag); } else { el = document.createElementNS(PREFIXES[tag.substring(0, qidx)], tag); } attribs && $attribs(el, attribs); body != null && $text(el, body); parent && $addChild(parent, el, idx); return el; }; const $comment = (body, parent, idx = -1) => { const comment = document.createComment( isString(body) ? body : body.length < 2 ? body[0] || "" : ["", ...body, ""].join("\n") ); parent && $addChild(parent, comment, idx); return comment; }; const $addChild = (parent, child, idx = -1) => { isNumber(idx) ? idx < 0 || idx >= parent.children.length ? parent.appendChild(child) : parent.insertBefore(child, parent.children[idx]) : parent.insertBefore(child, idx); }; const $remove = (el) => el?.remove(); const $moveTo = (newParent, el, idx = -1) => { $remove(el); $addChild(newParent, el, idx); }; const $clear = (el) => (el.innerHTML = "", el); const $text = (el, body) => { body = deref(body); el.textContent = body !== void 0 ? String(body) : ""; }; const $html = (el, body) => { el.innerHTML = String(deref(body)); }; const $attribs = (el, attribs) => { for (let id in attribs) { __setAttrib(el, id, attribs[id], attribs); } return el; }; const __setterCache = {}; const __getProto = Object.getPrototypeOf; const __getDesc = Object.getOwnPropertyDescriptor; const __desc = (proto, prop) => proto ? __getDesc(proto, prop) ?? __desc(__getProto(proto), prop) : void 0; const __setter = (el, prop) => { const key = `${el.namespaceURI}/${el.tagName}#${prop}`; return __setterCache[key] ?? (__setterCache[key] = __desc(__getProto(el), prop)?.set ?? false); }; const __setAttrib = (el, id, val, attribs) => { implementsFunction(val, "deref") && (val = val.deref()); if (id.startsWith("on")) { if (isString(val)) { el.setAttribute(id, val); } else { id = id.substring(2); isArray(val) ? el.addEventListener(id, val[0], val[1]) : el.addEventListener(id, val); } return; } isFunction(val) && (val = val(attribs)); isArray(val) && (val = val.join(ATTRIB_JOIN_DELIMS[id] || " ")); switch (id) { case "class": el.setAttribute( "class", isString(val) ? val : mergeClasses(el.className, val) ); break; case "style": $style(el, val); break; case "value": __updateValueAttrib(el, val); break; case "data": __updateDataAttribs(el, val); break; case "prefix": el.setAttribute(id, isString(val) ? val : formatPrefixes(val)); break; default: { if (val != null) { const setter = __setter(el, id); if (setter) return setter.call(el, val); } const idx = id.indexOf(":"); if (idx < 0) { val === false || val == null ? el.removeAttribute(id) : el.setAttribute(id, val === true ? id : val); } else { const ns = PREFIXES[id.substring(0, idx)]; val === false || val == null ? el.removeAttributeNS(ns, id) : el.setAttributeNS(ns, id, val); } } } }; const __updateValueAttrib = (el, value) => { const type = el instanceof HTMLTextAreaElement ? "text" : el.type; switch (type) { case "text": case "textarea": case "password": case "search": case "number": case "url": case "tel": if (el.value !== void 0 && isString(value)) { const start = el.selectionStart; const end = el.selectionEnd; el.value = value; !(el.disabled || el.readOnly) && el.setSelectionRange(start, end); break; } default: el.value = value; } }; const __updateDataAttribs = (el, attribs) => { const data = el.dataset; for (let id in attribs) { const v = deref(attribs[id]); data[id] = isFunction(v) ? v(attribs) : v; } }; const $style = (el, rules) => { if (isString(rules)) { el.setAttribute("style", rules); } else { const style = el.style; for (let id in rules) { let v = deref(rules[id]); isFunction(v) && (v = v(rules)); style.setProperty(id, v != null ? v : ""); } } }; const $toggleClasses = (el, ...classes) => { const list = el.classList; for (let c of classes) { list.contains(c) ? list.remove(c) : list.add(c); } }; const PREFIXES = { svg: XML_SVG, xlink: XML_XLINK, xmlns: XML_XMLNS }; const registerPrefix = (prefix, url) => { assert( !PREFIXES[prefix], `${prefix} already registered: ${PREFIXES[prefix]}` ); PREFIXES[prefix] = url; }; export { $addChild, $attribs, $clear, $comment, $el, $html, $moveTo, $remove, $style, $text, $toggleClasses, $tree, registerPrefix };