/*! * Statum - client-side state library (document module). * * Everything DOM. statum-core.js is headless (state, directives, SSE, workers, * persistence — no DOM at all); this module owns the presentation side and * installs itself into core through the module bridge at load time: * * - the render walk: expands `stm-for` loops and `stm-if` chains, applies * the structural and DOM-level `stm-*` bindings (`stm-class.*`, * `stm-attribute.*`, `stm-disabled`, `stm-text`, `stm-html`), wires * `stm-action` elements, and recurses into child elements. The * structural attributes (`stm-for`, `stm-if`/`stm-else-if`/`stm-else` and * `stm-local-*` / `stm-declare-*` variables) stay hardcoded in the walk: they build the scope and the * element list every handler receives. * - the attribute-handler REGISTRY and the public `statum.registerAttribute` * seam: module authors register `stm-*` handlers here; the walk iterates * them in resolved order. * - boot: reads the `stm-entrypoint` / `stm-slots` / `stm-channel` / * `stm-worker` `
` attributes into `statum.config` (core's own * mutable config object — no new API), manages the `stm-preloader` / * `stm-content` boot visibility, injects the `hidden` stylesheet and * warns about leftover server-side `pstm-*` markup. * - guards: `stm-guard` links, evaluated through core's afterRenderCycle * hook (redirected pages stop rendering). * - error/success state: `stm-on-error` / `stm-on-success` classes on action * elements, applied through core's reportFailure path. * - forms (merged from the former statum-forms.js): `stm-prefill` * (state -> control PROPERTY writes with dirty-element tracking), * `stm-clear-on-success` / `stm-clear-on-error` and the `statum.document` * API. * * stm-prefill writes properties, never attributes — that is its entire point. * `stm-attribute.value` keeps writing the value ATTRIBUTE (the server-wins * default-dirty path, issue C2); stm-prefill instead updates the control's * live `.value` / `.checked` / `.selected` properties so a prefill never * fights the user's typing. Elements the user has edited (dirty elements) are * never overwritten by a later render cycle. * * Radio groups are prefilled per-element: a radio carrying stm-prefill is * checked iff its own value matches the resolved value. Give every member of * the group the same stm-prefill expression and the group behaves correctly. * * Load order: SECOND - requires statum-core.js, which creates `window.statum` * and the `_bridge` this module registers through. Like core: an IIFE, no * dependencies, no build step, plain script tag. */ (function () { 'use strict'; if (!window.statum || !window.statum._bridge) { console.error('[statum] statum-document.js requires statum-core.js'); return; } // Core machinery shared through the module seam (see statum-core.js). // Aliased so the moved code keeps its original call shapes. var bridge = window.statum._bridge; var evalExpr = bridge.evalExpr; var childScope = bridge.childScope; var stateScope = bridge.stateScope; var triggerConfirm = bridge.triggerConfirm; var performAction = bridge.performAction; // --------------------------------------------------------------------------- // Attribute-handler registry (module seam) // --------------------------------------------------------------------------- // // `_bridge` (underscore = private by convention, like `_testMarkdown`) is // the internal cross-module surface; `statum.registerAttribute` is the // public entry point module authors use. /** * Resolved execution order of the per-element `stm-*` bindings: an ordered * list of `{name, fn, prefix}`. The render walk below iterates it for every * element. A throwing handler aborts that render cycle through renderAll's * try/catch, exactly as the previous hardcoded call chain did, so handlers * get no per-handler try/catch here. * @type {Array<{name: string, fn: Function, prefix: boolean}>} */ var attributeHandlers = []; /** * Register a handler for an `stm-*` attribute. * * `name` is either an exact attribute name (`'stm-disabled'`) or, when it * ends with a dot, a prefix matcher (`'stm-class.'` matches any * `stm-class.{name}`). The handler is called once per element per render * cycle when the element carries at least one matching attribute; it then * iterates the element's attributes itself (several may match a prefix). * * `opts.after` / `opts.before` name another registered attribute and the * handler is inserted directly after / before it. A constraint naming an * unregistered attribute warns and keeps plain registration order, so * loading a module early cannot break the page. Registering the same name * twice warns and ignores the second handler. * * @param {string} name * @param {Function} fn `fn(el, scope)` * @param {{after?: string, before?: string}} [opts] */ function registerAttribute(name, fn, opts) { if (typeof name !== 'string' || !name || typeof fn !== 'function') { console.warn('[statum] registerAttribute: expected an attribute name and a handler function'); return; } for (var i = 0; i < attributeHandlers.length; i++) { if (attributeHandlers[i].name === name) { console.warn('[statum] registerAttribute: "' + name + '" is already registered; ignoring'); return; } } var entry = { name: name, fn: fn, prefix: name.charAt(name.length - 1) === '.' }; var anchor = opts && (opts.after || opts.before); var at = anchor ? handlerIndex(anchor) : -1; if (anchor && at === -1) { console.warn('[statum] registerAttribute: "' + anchor + '" is not registered; keeping registration order for "' + name + '"'); } if (at !== -1 && opts.after) at += 1; // insert directly after the anchor if (at === -1) attributeHandlers.push(entry); else attributeHandlers.splice(at, 0, entry); } /** Position of a registered handler by name, or -1. */ function handlerIndex(name) { for (var i = 0; i < attributeHandlers.length; i++) { if (attributeHandlers[i].name === name) return i; } return -1; } // --------------------------------------------------------------------------- // Rendering: loops, conditionals, bindings // --------------------------------------------------------------------------- /** Templates captured from loop holders. @type {WeakMap