AkurAI Build
Menu

Public repository / master

AkurAI-Mail / src/framework/dom.js

Latest change 3af23b0e937520000e1aa43c7e2eb9de2675abd1 - OLI-248: webmail list and reader as framework islands on a signals store by Ólafur Búi Ólafsson

// dom.js — tiny DOM builder for island render functions (plain ESM, no build step).
//
//   h("button", { class: "icon-button", "aria-label": "Close", onclick: close }, icon("X"), " Close")
//
// Attributes: `class`, `aria-*`, `data-*` and any string attribute; `on<event>` keys bind listeners;
// `false`/`null`/`undefined` values skip the attribute; `true` sets it empty. Children may be
// strings, nodes, arrays or null (skipped). Text children are text nodes, never HTML.

/**
 * @param {string} tag
 * @param {Record<string, any> | null} [attrs]
 * @param {...any} children
 * @returns {HTMLElement}
 */
export function h(tag, attrs, ...children) {
  const el = document.createElement(tag);
  for (const [key, value] of Object.entries(attrs ?? {})) {
    if (value === false || value === null || value === undefined) continue;
    if (key.startsWith("on") && typeof value === "function") el.addEventListener(key.slice(2), value);
    else if (key === "style" && typeof value === "object") Object.assign(el.style, value);
    else if (key === "value" && "value" in el) /** @type {any} */ (el).value = value;
    else if (key === "checked" && "checked" in el) /** @type {any} */ (el).checked = !!value;
    else el.setAttribute(key, value === true ? "" : String(value));
  }
  append(el, children);
  return el;
}

/**
 * @param {Node} el
 * @param {any} children
 */
export function append(el, children) {
  for (const child of Array.isArray(children) ? children : [children]) {
    if (child === null || child === undefined || child === false) continue;
    if (Array.isArray(child)) append(el, child);
    else el.appendChild(child instanceof Node ? child : document.createTextNode(String(child)));
  }
}

/**
 * Replaces an element's children in one step.
 * @param {Element} el
 * @param {any} children
 */
export function replace(el, children) {
  el.replaceChildren();
  append(el, children);
}

/**
 * Sanitized HTML fragment: only the markup the rich-text sanitizer allows gets through.
 * @param {string} html
 * @param {(html: string) => string} sanitize
 */
export function fragment(html, sanitize) {
  const tpl = document.createElement("template");
  tpl.innerHTML = sanitize(html);
  return tpl.content;
}