// A region that is out only while a control holds a value. // // `079a011e`, ruled 2026-08-25: the condition lives on the region. A region // names the control it watches and the value that brings it out, and every // renderer answers it from what it already has. This is the browser's half, and // it makes no request: the bytes are in the document, and a round trip to // reveal a section of a form the reader is midway through would re-render boxes // holding values they have not committed yet. // // It reads three attributes the node emitter writes and nothing else: // `data-reveal` names the control, `data-reveal-when` says whether the region // wants anything, nothing or one of a set of values, and `data-reveal-values` // carries that set as JSON. All three are ordinary escaped attributes, so // nothing here is a program built out of app text -- which is why the behaviour // is a script rather than one of the emitted _hyperscript programs, whose one // rule is that no program is written from text a user typed. // // A page that does not serve this file shows every conditional region, which is // the direction everything else in this renderer degrades in: more content // rather than less. The six hand-written toggles this replaces fail the other // way, since their markup starts hidden. (() => { "use strict"; /** The control a region watches. */ const CONTROL = "data-reveal"; /** What that control has to hold: `any`, `none` or `value`. */ const WHEN = "data-reveal-when"; /** The values `value` accepts, as a JSON array. */ const VALUES = "data-reveal-values"; /** * What the named control is holding now, or null when it holds nothing. * * By `name` rather than by `id`, because `name` is what the description * carries: it is what a submit sends the value under and what a region * names. An id is scoped per form instance by the emitter and is therefore * not the string the description wrote. * * Searched from the region outward: the nearest enclosing form first, then * the document, which is `fill.js`'s rule and is here for the same reason. * A screen showing the same form twice -- an edit modal over a list -- * otherwise reads the first copy in the document whichever one the reader * is in. * * A tick is there by presence, the way a form submits one, so an unticked * box and an empty box are one answer. That is the convention the router * states and the other two renderers hold. */ const held = (region, name) => { const selector = `[name="${CSS.escape(name)}"]`; const scope = region.closest("form") ?? document; const controls = scope.querySelectorAll(selector); let value = null; for (const control of controls) { if (control.type === "checkbox" || control.type === "radio") { if (control.checked) { value = control.value === "" ? "on" : control.value; } continue; } value = control.value ?? null; } return value === "" ? null : value; }; /** Whether a region asking for this is satisfied by what is held. */ const satisfied = (region, value) => { const when = region.getAttribute(WHEN); if (when === "any") { return value !== null; } if (when === "none") { return value === null; } if (value === null) { return false; } let wanted; try { wanted = JSON.parse(region.getAttribute(VALUES) ?? "[]"); } catch { // An attribute this file did not write. Nothing is revealed on a // condition nobody can read, which is the same answer a region // watching a control that is not on the screen gets. return false; } return Array.isArray(wanted) && wanted.includes(value); }; /** * Put every conditional region where its condition says it belongs. * * A walk over the document rather than over a list captured when the page * loaded, for the reason the clock gives: a swap brings new regions and * takes old ones away, and both are found by the next pass. */ const settle = () => { for (const region of document.querySelectorAll(`[${CONTROL}]`)) { const value = held(region, region.getAttribute(CONTROL) ?? ""); region.hidden = !satisfied(region, value); } }; // Delegated, and on both events a control settles under: `change` is what a // checkbox and a select fire, `input` is what a box being typed into fires, // and a region may watch either. document.addEventListener("change", settle); document.addEventListener("input", settle); // A region that arrives mid-page should not wait for the reader to touch // something before it decides whether it applies. htmx's event and the // initial parse both land here. // // `htmx:after:settle` is htmx 4's name for it; 2.x spelled the same event // `htmx:afterSettle`, and 4 renamed every event to phase:action. document.addEventListener("htmx:after:settle", settle); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", settle); } else { settle(); } })();