morse-code / views /dom.js
RemiFabre
ui: compact, denser layout so message + transcript fit together
4f5e7fd
Raw
History Blame Contribute Delete
1.95 kB
/** Tiny DOM helpers (no framework). */
/**
* el("button.cls#id", {onclick, ...attrs}, [children|string])
* Tag is optional (defaults to div); `.class` and `#id` tokens may appear in
* any order and any count after it (e.g. "section.view#view-compose").
*/
export function el(spec, attrs = {}, children = []) {
const tagMatch = spec.match(/^[a-z][a-z0-9]*/i);
const tag = tagMatch ? tagMatch[0] : "div";
const node = document.createElement(tag);
const rest = spec.slice(tagMatch ? tagMatch[0].length : 0);
const idMatch = rest.match(/#([\w-]+)/);
if (idMatch) node.id = idMatch[1];
const classes = [...rest.matchAll(/\.([\w-]+)/g)].map((x) => x[1]);
if (classes.length) node.className = classes.join(" ");
for (const [k, v] of Object.entries(attrs)) {
if (v == null || v === false) continue;
if (k.startsWith("on") && typeof v === "function") {
node.addEventListener(k.slice(2).toLowerCase(), v);
} else if (k === "class") {
node.className = node.className ? `${node.className} ${v}` : v;
} else if (k === "html") {
node.innerHTML = v;
} else if (k in node && k !== "list") {
try { node[k] = v; } catch { node.setAttribute(k, v); }
} else {
node.setAttribute(k, v);
}
}
const kids = Array.isArray(children) ? children : [children];
for (const c of kids) {
if (c == null || c === false) continue;
node.append(c.nodeType ? c : document.createTextNode(String(c)));
}
return node;
}
export function clear(node) {
while (node.firstChild) node.removeChild(node.firstChild);
return node;
}
/** Render a morse pattern string ('.-') as styled dot/dash chips. */
export function morsePattern(pattern) {
const wrap = el("span.pattern");
for (const ch of pattern) {
wrap.append(el(`span.${ch === "." ? "dot" : "dash"}`));
}
return wrap;
}