43 lines
1.8 KiB
JavaScript
43 lines
1.8 KiB
JavaScript
// Shared DOM helpers used by every view. `el` builds nodes without innerHTML
|
|
// injection; the rest are the small conveniences the views repeat.
|
|
export function el(tag, attrs = {}, ...children) {
|
|
const node = document.createElement(tag);
|
|
for (const [key, value] of Object.entries(attrs)) {
|
|
if (value == null || value === false) continue;
|
|
if (key === "class") node.className = value;
|
|
else if (key === "dataset") Object.assign(node.dataset, value);
|
|
else if (key.startsWith("on") && typeof value === "function")
|
|
node.addEventListener(key.slice(2).toLowerCase(), value);
|
|
else node.setAttribute(key, value);
|
|
}
|
|
for (const child of children.flat()) {
|
|
if (child == null || child === false) continue;
|
|
node.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
export function errorBanner(message) {
|
|
return el("div", { class: "alert", role: "alert" }, message);
|
|
}
|
|
|
|
/**
|
|
* Replace a container's contents, dropping the absent ones.
|
|
*
|
|
* `el` already ignores null children, but `replaceChildren` does not: it stringifies
|
|
* them, so an optional node that is not there renders the word "null" on the page.
|
|
* Every view builds optional nodes — a banner only while a job runs, a conflict only
|
|
* after a 409 — so the filter belongs here rather than in each caller. It was missing
|
|
* from one of them, and a documentation screenshot is where that turned up (US09-04).
|
|
*/
|
|
export function show(root, ...nodes) {
|
|
root.replaceChildren(...nodes.flat().filter((node) => node != null && node !== false));
|
|
}
|
|
|
|
export function setActiveNav(view) {
|
|
document.querySelectorAll("nav a[data-nav]").forEach((a) => {
|
|
if (a.dataset.nav === view) a.setAttribute("aria-current", "page");
|
|
else a.removeAttribute("aria-current");
|
|
});
|
|
}
|