@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
JavaScript
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
};