UNPKG

uhtml

Version:

A minimalistic library to create fast and reactive Web pages

193 lines (171 loc) 5.95 kB
import DEBUG from '../debug.js'; import errors from '../errors.js'; import { ATTRIBUTE as TEMPLATE_ATTRIBUTE, COMMENT as TEMPLATE_COMMENT, COMPONENT as TEMPLATE_COMPONENT, TEXT as TEMPLATE_TEXT, children, } from './ish.js'; import { Signal } from './signals.js'; import { Unsafe, assign, entries, isArray } from '../utils.js'; import { PersistentFragment, diffFragment, nodes } from './persistent-fragment.js'; import creator from './creator.js'; import diff from './diff.js'; export const ARRAY = 1 << 0; export const ARIA = 1 << 1; export const ATTRIBUTE = 1 << 2; export const COMMENT = 1 << 3; export const COMPONENT = 1 << 4; export const DATA = 1 << 5; export const DIRECT = 1 << 6; export const DOTS = 1 << 7; export const EVENT = 1 << 8; export const KEY = 1 << 9; export const PROP = 1 << 10; export const TEXT = 1 << 11; export const TOGGLE = 1 << 12; export const UNSAFE = 1 << 13; export const REF = 1 << 14; export const SIGNAL = 1 << 15; // COMPONENT flags const COMPONENT_DIRECT = COMPONENT | DIRECT; const COMPONENT_DOTS = COMPONENT | DOTS; const COMPONENT_PROP = COMPONENT | PROP; const EVENT_ARRAY = EVENT | ARRAY; const COMMENT_ARRAY = COMMENT | ARRAY; export const fragment = creator(document); export const ref = Symbol('ref'); const aria = (node, values) => { for (const [key, value] of entries(values)) { const name = key === 'role' ? key : `aria-${key.toLowerCase()}`; if (value == null) node.removeAttribute(name); else node.setAttribute(name, value); } }; const attribute = name => (node, value) => { if (value == null) node.removeAttribute(name); else node.setAttribute(name, value); }; const comment_array = (node, value) => { node[nodes] = diff( node[nodes] || children, value, diffFragment, node ); }; const text = new WeakMap; const getText = (ref, value) => { let node = text.get(ref); if (node) node.data = value; else text.set(ref, (node = document.createTextNode(value))); return node; }; const comment_hole = (node, value) => { const current = typeof value === 'object' ? (value ?? node) : getText(node, value); const prev = node[nodes] ?? node; if (current !== prev) prev.replaceWith(diffFragment(node[nodes] = current, 1)); }; const comment_unsafe = xml => (node, value) => { const prev = node[ref] ?? (node[ref] = {}); if (prev.v !== value) { prev.f = PersistentFragment(fragment(value, xml)); prev.v = value; } comment_hole(node, prev.f); }; const comment_signal = (node, value) => { comment_hole(node, value instanceof Signal ? value.value : value); }; const data = ({ dataset }, values) => { for (const [key, value] of entries(values)) { if (value == null) delete dataset[key]; else dataset[key] = value; } }; /** @type {Map<string|Symbol, Function>} */ const directRefs = new Map; /** * @param {string|Symbol} name * @returns {Function} */ const directFor = name => { let fn = directRefs.get(name); if (!fn) directRefs.set(name, (fn = direct(name))); return fn; }; const direct = name => (node, value) => { node[name] = value; }; const dots = (node, values) => { for (const [name, value] of entries(values)) attribute(name)(node, value); }; const event = (type, at, array) => array ? ((node, value) => { const prev = node[at]; if (prev?.length) node.removeEventListener(type, ...prev); if (value) node.addEventListener(type, ...value); node[at] = value; }) : ((node, value) => { const prev = node[at]; if (prev) node.removeEventListener(type, prev); if (value) node.addEventListener(type, value); node[at] = value; }) ; const toggle = name => (node, value) => { node.toggleAttribute(name, !!value); }; let k = false; export const isKeyed = () => { const wasKeyed = k; k = false; return wasKeyed; }; export const update = (node, type, path, name, hint) => { switch (type) { case TEMPLATE_COMPONENT: return [path, hint, COMPONENT]; case TEMPLATE_COMMENT: { if (isArray(hint)) return [path, comment_array, COMMENT_ARRAY]; if (hint instanceof Unsafe) return [path, comment_unsafe(node.xml), UNSAFE]; if (hint instanceof Signal) return [path, comment_signal, COMMENT | SIGNAL]; return [path, comment_hole, COMMENT]; } case TEMPLATE_TEXT: return [path, directFor('textContent'), TEXT]; case TEMPLATE_ATTRIBUTE: { const isComponent = node.type === TEMPLATE_COMPONENT; switch (name.at(0)) { case '@': { if (DEBUG && isComponent) throw errors.invalid_attribute([], name); const array = isArray(hint); return [path, event(name.slice(1), Symbol(name), array), array ? EVENT_ARRAY : EVENT]; } case '?': if (DEBUG && isComponent) throw errors.invalid_attribute([], name); return [path, toggle(name.slice(1)), TOGGLE]; case '.': { return name === '...' ? [path, isComponent ? assign : dots, isComponent ? COMPONENT_DOTS : DOTS] : [path, direct(name.slice(1)), isComponent ? COMPONENT_DIRECT : DIRECT] ; } default: { if (isComponent) return [path, direct(name), COMPONENT_PROP]; if (name === 'aria') return [path, aria, ARIA]; if (name === 'data' && !/^object$/i.test(node.name)) return [path, data, DATA]; if (name === 'key') { if (DEBUG && 1 < path.length) throw errors.invalid_key(hint); return [path, (k = true), KEY]; }; if (name === 'ref') return [path, directFor(ref), REF]; if (name.startsWith('on')) return [path, directFor(name.toLowerCase()), DIRECT]; return [path, attribute(name), ATTRIBUTE]; } } } } };