max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+8 insertions,
-469 deletions
| @@ -8,13 +8,19 @@ | |||
| 8 | 8 | multithreaded/static/dist/ | |
| 9 | 9 | multithreaded/frontend/node_modules/ | |
| 10 | 10 | ||
| 11 | - | # Generated stylesheets (makeover-build emits these from makeover-geometry and | |
| 12 | - | # makeover-webview; the crates are the source, a checked-in copy would drift) | |
| 11 | + | # Generated stylesheets and scripts (makeover-build and quasi-webview emit | |
| 12 | + | # these; the crates are the source, a checked-in copy would drift). Every | |
| 13 | + | # script quasi-webview ships is named here, not some of them: three of the | |
| 14 | + | # eight were tracked until 2026-08-27, which is the drift this rule exists to | |
| 15 | + | # prevent sitting in the tree waiting to happen. | |
| 13 | 16 | server/static/quasi-selection.js | |
| 14 | 17 | server/static/quasi-clock.js | |
| 15 | 18 | server/static/quasi-download.js | |
| 16 | 19 | server/static/quasi-fill.js | |
| 17 | 20 | server/static/quasi-copy.js | |
| 21 | + | server/static/quasi-reveal.js | |
| 22 | + | server/static/quasi-repeat.js | |
| 23 | + | server/static/quasi-awaiting.js | |
| 18 | 24 | server/static/geometry.css | |
| 19 | 25 | server/static/timing.css | |
| 20 | 26 | server/static/layout.css |
| @@ -1,140 +1,0 @@ | |||
| 1 | - | // The half of a wait that only the browser can see. | |
| 2 | - | // | |
| 3 | - | // The node emitter has written `data-awaiting` since the vocabulary gained | |
| 4 | - | // `Awaiting`: `determinate` with a `data-awaiting-amount` beside it, or | |
| 5 | - | // `indeterminate` alone. `makeover-webview` 0.60.0 draws both. Neither of them | |
| 6 | - | // knows when a wait is actually running, and that is what this file supplies. | |
| 7 | - | // | |
| 8 | - | // Three things, and nothing else: | |
| 9 | - | // | |
| 10 | - | // 1. `aria-busy` on the element that made the request, for as long as the | |
| 11 | - | // request is in flight. That is what the design system keys the mark off, | |
| 12 | - | // and it is a standard attribute rather than a class of ours, so it is | |
| 13 | - | // announced as well as drawn. | |
| 14 | - | // 2. `--awaiting-share`, for a control whose wait has a measured size, from | |
| 15 | - | // bytes actually observed arriving. Never from elapsed time: see below. | |
| 16 | - | // 3. Nothing at all for a control that is not marked as awaiting, which is | |
| 17 | - | // nearly all of them. | |
| 18 | - | // | |
| 19 | - | // # Why the attribute alone was not enough | |
| 20 | - | // | |
| 21 | - | // `data-awaiting` is a fact about the control -- pressing this waits -- and it | |
| 22 | - | // is true when the page is painted and stays true. Styling it directly would | |
| 23 | - | // put a blinking mark on every awaiting control on the screen from the moment | |
| 24 | - | // the document loaded. The running-or-not half is true only between two events, | |
| 25 | - | // which makes it the binder's, which is this. | |
| 26 | - | // | |
| 27 | - | // # What the share may not be | |
| 28 | - | // | |
| 29 | - | // Rule 1 of wiki `loading-and-progress-standard`, and `Awaiting`'s own docs | |
| 30 | - | // before it: what is done over what there is, and never a remaining time, an | |
| 31 | - | // arrival time, or a rate extrapolated forward. So the share is set from | |
| 32 | - | // `event.loaded` -- bytes the browser says have landed -- and from nothing | |
| 33 | - | // else. A control with no readable progress keeps a share of zero and draws an | |
| 34 | - | // empty trough, which is the honest picture: the size is known and the delivery | |
| 35 | - | // is not. | |
| 36 | - | // | |
| 37 | - | // The one thing this file must never grow is a timer that advances the share. | |
| 38 | - | // A bar walking forward on a stalled transfer is the confidently-wrong drawing | |
| 39 | - | // the rule exists to forbid, and it is the easiest thing in the world to add. | |
| 40 | - | // | |
| 41 | - | // # Degrading | |
| 42 | - | // | |
| 43 | - | // A page that does not serve this file draws no mark and no bar, and every | |
| 44 | - | // control still locks itself through `hx-disable` exactly as before. That is | |
| 45 | - | // the direction the rest of this renderer degrades in: less decoration, never | |
| 46 | - | // less behaviour. | |
| 47 | - | (() => { | |
| 48 | - | "use strict"; | |
| 49 | - | ||
| 50 | - | /** The fact about the control: this call waits. */ | |
| 51 | - | const AWAITING = "data-awaiting"; | |
| 52 | - | /** The measured payload size, when the description carried one. */ | |
| 53 | - | const AMOUNT = "data-awaiting-amount"; | |
| 54 | - | /** How much of it has landed, as a share from 0 to 1. */ | |
| 55 | - | const SHARE = "--awaiting-share"; | |
| 56 | - | ||
| 57 | - | /** | |
| 58 | - | * The element a request belongs to, or null when it is not our business. | |
| 59 | - | * | |
| 60 | - | * htmx names the element on the event detail. An event for something that | |
| 61 | - | * never described a wait is left alone rather than given one: the emitter | |
| 62 | - | * decides which calls are worth marking, and a renderer that marked every | |
| 63 | - | * request would be overruling it. | |
| 64 | - | */ | |
| 65 | - | const marked = (event) => { | |
| 66 | - | const element = event.detail?.elt; | |
| 67 | - | if (!element || typeof element.hasAttribute !== "function") { | |
| 68 | - | return null; | |
| 69 | - | } | |
| 70 | - | return element.hasAttribute(AWAITING) ? element : null; | |
| 71 | - | }; | |
| 72 | - | ||
| 73 | - | /** Say a wait has started, with nothing delivered yet. */ | |
| 74 | - | const begin = (event) => { | |
| 75 | - | const element = marked(event); | |
| 76 | - | if (!element) { | |
| 77 | - | return; | |
| 78 | - | } | |
| 79 | - | element.setAttribute("aria-busy", "true"); | |
| 80 | - | element.style.removeProperty(SHARE); | |
| 81 | - | }; | |
| 82 | - | ||
| 83 | - | /** | |
| 84 | - | * Say the wait is over. | |
| 85 | - | * | |
| 86 | - | * The share is cleared with it. A control pressed twice would otherwise | |
| 87 | - | * start its second wait wearing the first one's bar, which is a number | |
| 88 | - | * about a payload that has already arrived. | |
| 89 | - | */ | |
| 90 | - | const end = (event) => { | |
| 91 | - | const element = marked(event); | |
| 92 | - | if (!element) { | |
| 93 | - | return; | |
| 94 | - | } | |
| 95 | - | element.removeAttribute("aria-busy"); | |
| 96 | - | element.style.removeProperty(SHARE); | |
| 97 | - | }; | |
| 98 | - | ||
| 99 | - | /** | |
| 100 | - | * What has landed, over what the description said there was. | |
| 101 | - | * | |
| 102 | - | * `event.loaded` when the browser is counting, against the described | |
| 103 | - | * amount rather than against `event.total`. The two usually agree, and | |
| 104 | - | * where they do not the description is the one the bar was drawn for: a | |
| 105 | - | * server that gzips a response reports a total the screen never described. | |
| 106 | - | * | |
| 107 | - | * A response with no readable length leaves the share alone, so the trough | |
| 108 | - | * stays empty rather than jumping to something invented. | |
| 109 | - | */ | |
| 110 | - | const progressed = (event) => { | |
| 111 | - | const element = marked(event); | |
| 112 | - | if (!element || element.getAttribute(AWAITING) !== "determinate") { | |
| 113 | - | return; | |
| 114 | - | } | |
| 115 | - | const amount = Number(element.getAttribute(AMOUNT)); | |
| 116 | - | const loaded = Number(event.detail?.loaded); | |
| 117 | - | if (!Number.isFinite(amount) || amount <= 0 || !Number.isFinite(loaded)) { | |
| 118 | - | return; | |
| 119 | - | } | |
| 120 | - | element.style.setProperty(SHARE, String(Math.min(loaded / amount, 1))); | |
| 121 | - | }; | |
| 122 | - | ||
| 123 | - | // htmx 4 spells every event `phase:action`; 2.x spelled these | |
| 124 | - | // `htmx:beforeRequest`, `htmx:afterRequest` and `htmx:xhr:progress`. | |
| 125 | - | // | |
| 126 | - | // Delegated on the document rather than bound per control, for the reason | |
| 127 | - | // the reveal and repeat scripts give: a swap brings new controls and takes | |
| 128 | - | // old ones away, and a listener on the document covers both without a | |
| 129 | - | // re-scan. | |
| 130 | - | document.addEventListener("htmx:before:request", begin); | |
| 131 | - | document.addEventListener("htmx:after:request", end); | |
| 132 | - | document.addEventListener("htmx:xhr:progress", progressed); | |
| 133 | - | ||
| 134 | - | // A request that is aborted or errors out never reaches `after:request` in | |
| 135 | - | // every htmx version, and a control left wearing `aria-busy` blinks for the | |
| 136 | - | // rest of the page's life. Cheap insurance, and the same handler. | |
| 137 | - | document.addEventListener("htmx:abort", end); | |
| 138 | - | document.addEventListener("htmx:response:error", end); | |
| 139 | - | document.addEventListener("htmx:send:error", end); | |
| 140 | - | })(); |
| @@ -1,204 +1,0 @@ | |||
| 1 | - | // A question answered N times, with the reader adding and removing the slots. | |
| 2 | - | // | |
| 3 | - | // `60d1753c`, ruled 2026-08-25: a repeating group enters the vocabulary and | |
| 4 | - | // submits once. This is the browser's half of the third hard part -- the reader | |
| 5 | - | // creates and destroys slots without a round trip on every one. The bytes are | |
| 6 | - | // already in the document: the emitter writes a `template` holding one blank | |
| 7 | - | // slot, and adding is cloning it. | |
| 8 | - | // | |
| 9 | - | // A round trip instead would re-render a form the reader is midway through, | |
| 10 | - | // which is `a135f898`, and it would ask a route for an empty box. | |
| 11 | - | // | |
| 12 | - | // It reads four attributes the node emitter writes and nothing else: | |
| 13 | - | // `data-repeat` names the question, `data-repeat-label` is what its slots are | |
| 14 | - | // called, and `data-repeat-least` / `data-repeat-most` are the floor and the | |
| 15 | - | // ceiling. All four are ordinary escaped attributes, so nothing here is a | |
| 16 | - | // program built out of app text -- which is why this is a script rather than | |
| 17 | - | // one of the emitted _hyperscript programs, whose one rule is that no program | |
| 18 | - | // is written from text a user typed. | |
| 19 | - | // | |
| 20 | - | // A page that does not serve this file shows every slot the description | |
| 21 | - | // offered, fillable and submittable, with two controls that do nothing. | |
| 22 | - | (() => { | |
| 23 | - | "use strict"; | |
| 24 | - | ||
| 25 | - | /** The question a fieldset repeats. */ | |
| 26 | - | const NAME = "data-repeat"; | |
| 27 | - | /** What one slot of it is called. */ | |
| 28 | - | const LABEL = "data-repeat-label"; | |
| 29 | - | /** The fewest slots that may stand. */ | |
| 30 | - | const LEAST = "data-repeat-least"; | |
| 31 | - | /** The most that may stand. Absent means no ceiling. */ | |
| 32 | - | const MOST = "data-repeat-most"; | |
| 33 | - | /** The box the slots sit in. */ | |
| 34 | - | const SLOTS = "data-repeat-slots"; | |
| 35 | - | /** One slot, and where in the order it currently sits. */ | |
| 36 | - | const AT = "data-repeat-at"; | |
| 37 | - | /** The control that adds one. */ | |
| 38 | - | const ADD = "data-repeat-add"; | |
| 39 | - | /** The control that takes one away. */ | |
| 40 | - | const REMOVE = "data-repeat-remove"; | |
| 41 | - | ||
| 42 | - | /** The attributes a rename has to follow the index through. */ | |
| 43 | - | const NAMED = ["name", "id", "for", "aria-describedby", "aria-labelledby"]; | |
| 44 | - | ||
| 45 | - | /** The group a control inside one belongs to. */ | |
| 46 | - | const groupOf = (element) => element.closest(`[${NAME}]`); | |
| 47 | - | ||
| 48 | - | /** Where a group keeps its slots. */ | |
| 49 | - | const boxOf = (group) => group.querySelector(`[${SLOTS}]`); | |
| 50 | - | ||
| 51 | - | /** The slots standing in a group, in document order. */ | |
| 52 | - | const slotsOf = (group) => [ | |
| 53 | - | ...(boxOf(group)?.querySelectorAll(`:scope > [${AT}]`) ?? []), | |
| 54 | - | ]; | |
| 55 | - | ||
| 56 | - | /** A number written in an attribute, or a fallback when it is not there. */ | |
| 57 | - | const count = (group, attribute, fallback) => { | |
| 58 | - | const written = group.getAttribute(attribute); | |
| 59 | - | if (written === null) { | |
| 60 | - | return fallback; | |
| 61 | - | } | |
| 62 | - | const value = Number.parseInt(written, 10); | |
| 63 | - | return Number.isNaN(value) ? fallback : value; | |
| 64 | - | }; | |
| 65 | - | ||
| 66 | - | /** | |
| 67 | - | * Move one slot to a place in the order. | |
| 68 | - | * | |
| 69 | - | * The index is in the slot's own attribute, in every name it submits under | |
| 70 | - | * and in the ordinal a person reads. All three are rewritten together, or a | |
| 71 | - | * removed slot leaves the ones after it answering under names nobody asked | |
| 72 | - | * for. | |
| 73 | - | * | |
| 74 | - | * The rename is a replacement of `question[old]` wherever it appears, which | |
| 75 | - | * covers the derived ids beside the name itself -- `question[0]-hint`, | |
| 76 | - | * `question[0]-error` -- without this having to know what the field | |
| 77 | - | * emitter derives. | |
| 78 | - | */ | |
| 79 | - | const renumber = (slot, question, label, to) => { | |
| 80 | - | const from = slot.getAttribute(AT); | |
| 81 | - | slot.setAttribute(AT, String(to)); | |
| 82 | - | if (from === null) { | |
| 83 | - | return; | |
| 84 | - | } | |
| 85 | - | const was = `${question}[${from}]`; | |
| 86 | - | const now = `${question}[${to}]`; | |
| 87 | - | if (was !== now) { | |
| 88 | - | for (const element of [slot, ...slot.querySelectorAll("*")]) { | |
| 89 | - | for (const attribute of NAMED) { | |
| 90 | - | const value = element.getAttribute(attribute); | |
| 91 | - | if (value !== null && value.includes(was)) { | |
| 92 | - | element.setAttribute(attribute, value.split(was).join(now)); | |
| 93 | - | } | |
| 94 | - | } | |
| 95 | - | } | |
| 96 | - | } | |
| 97 | - | // The visible ordinal, which `Repeat::ordinal` wrote as "<label> <n>" | |
| 98 | - | // counting from one. Rewritten from the group's own label rather than | |
| 99 | - | // by editing whatever text is in there, so a slot cannot end up named | |
| 100 | - | // after the one it replaced. | |
| 101 | - | if (label !== null) { | |
| 102 | - | const named = slot.querySelector("label[for]"); | |
| 103 | - | if (named !== null) { | |
| 104 | - | named.textContent = `${label} ${to + 1}`; | |
| 105 | - | } | |
| 106 | - | } | |
| 107 | - | }; | |
| 108 | - | ||
| 109 | - | /** | |
| 110 | - | * Put a group's slots and its two controls back in agreement. | |
| 111 | - | * | |
| 112 | - | * Called after every change and on arrival, so a group the reader has not | |
| 113 | - | * touched is in the same state as one they have. | |
| 114 | - | */ | |
| 115 | - | const settle = (group) => { | |
| 116 | - | const question = group.getAttribute(NAME) ?? ""; | |
| 117 | - | const label = group.getAttribute(LABEL); | |
| 118 | - | const slots = slotsOf(group); | |
| 119 | - | const least = count(group, LEAST, 0); | |
| 120 | - | const most = count(group, MOST, Number.POSITIVE_INFINITY); | |
| 121 | - | slots.forEach((slot, at) => renumber(slot, question, label, at)); | |
| 122 | - | for (const control of group.querySelectorAll(`[${REMOVE}]`)) { | |
| 123 | - | control.disabled = slots.length <= least; | |
| 124 | - | } | |
| 125 | - | for (const control of group.querySelectorAll(`[${ADD}]`)) { | |
| 126 | - | control.disabled = slots.length >= most; | |
| 127 | - | } | |
| 128 | - | }; | |
| 129 | - | ||
| 130 | - | /** Every group in the document, settled. */ | |
| 131 | - | const settleAll = () => { | |
| 132 | - | for (const group of document.querySelectorAll(`[${NAME}]`)) { | |
| 133 | - | settle(group); | |
| 134 | - | } | |
| 135 | - | }; | |
| 136 | - | ||
| 137 | - | /** Clone the blank slot onto the end, if the ceiling allows another. */ | |
| 138 | - | const add = (group) => { | |
| 139 | - | const blank = group.querySelector("template"); | |
| 140 | - | const box = boxOf(group); | |
| 141 | - | if (blank === null || box === null) { | |
| 142 | - | return; | |
| 143 | - | } | |
| 144 | - | const most = count(group, MOST, Number.POSITIVE_INFINITY); | |
| 145 | - | if (slotsOf(group).length >= most) { | |
| 146 | - | return; | |
| 147 | - | } | |
| 148 | - | const made = blank.content.cloneNode(true); | |
| 149 | - | const slot = made.firstElementChild; | |
| 150 | - | box.append(made); | |
| 151 | - | settle(group); | |
| 152 | - | // The reader pressed add because they have something to type, so the | |
| 153 | - | // caret goes where they meant it to. Nothing else in this file moves | |
| 154 | - | // focus: a settle after a swap must not steal it. | |
| 155 | - | slot?.querySelector("input, select, textarea")?.focus(); | |
| 156 | - | }; | |
| 157 | - | ||
| 158 | - | /** Take one slot out, if the floor allows one fewer. */ | |
| 159 | - | const remove = (group, slot) => { | |
| 160 | - | const least = count(group, LEAST, 0); | |
| 161 | - | if (slotsOf(group).length <= least) { | |
| 162 | - | return; | |
| 163 | - | } | |
| 164 | - | slot.remove(); | |
| 165 | - | settle(group); | |
| 166 | - | }; | |
| 167 | - | ||
| 168 | - | // Delegated, so a group that arrives in a swap needs no wiring of its own. | |
| 169 | - | document.addEventListener("click", (event) => { | |
| 170 | - | const target = event.target; | |
| 171 | - | if (!(target instanceof Element)) { | |
| 172 | - | return; | |
| 173 | - | } | |
| 174 | - | const adder = target.closest(`[${ADD}]`); | |
| 175 | - | if (adder !== null) { | |
| 176 | - | const group = groupOf(adder); | |
| 177 | - | if (group !== null) { | |
| 178 | - | event.preventDefault(); | |
| 179 | - | add(group); | |
| 180 | - | } | |
| 181 | - | return; | |
| 182 | - | } | |
| 183 | - | const remover = target.closest(`[${REMOVE}]`); | |
| 184 | - | if (remover === null) { | |
| 185 | - | return; | |
| 186 | - | } | |
| 187 | - | const group = groupOf(remover); | |
| 188 | - | const slot = remover.closest(`[${AT}]`); | |
| 189 | - | if (group !== null && slot !== null) { | |
| 190 | - | event.preventDefault(); | |
| 191 | - | remove(group, slot); | |
| 192 | - | } | |
| 193 | - | }); | |
| 194 | - | ||
| 195 | - | // A group that arrives mid-page settles like one that was parsed with the | |
| 196 | - | // document. `htmx:after:settle` is htmx 4's name for it; 2.x spelled the | |
| 197 | - | // same event `htmx:afterSettle`. | |
| 198 | - | document.addEventListener("htmx:after:settle", settleAll); | |
| 199 | - | if (document.readyState === "loading") { | |
| 200 | - | document.addEventListener("DOMContentLoaded", settleAll); | |
| 201 | - | } else { | |
| 202 | - | settleAll(); | |
| 203 | - | } | |
| 204 | - | })(); |
| @@ -1,123 +1,0 @@ | |||
| 1 | - | // A region that is out only while a control holds a value. | |
| 2 | - | // | |
| 3 | - | // `079a011e`, ruled 2026-08-25: the condition lives on the region. A region | |
| 4 | - | // names the control it watches and the value that brings it out, and every | |
| 5 | - | // renderer answers it from what it already has. This is the browser's half, and | |
| 6 | - | // it makes no request: the bytes are in the document, and a round trip to | |
| 7 | - | // reveal a section of a form the reader is midway through would re-render boxes | |
| 8 | - | // holding values they have not committed yet. | |
| 9 | - | // | |
| 10 | - | // It reads three attributes the node emitter writes and nothing else: | |
| 11 | - | // `data-reveal` names the control, `data-reveal-when` says whether the region | |
| 12 | - | // wants anything, nothing or one of a set of values, and `data-reveal-values` | |
| 13 | - | // carries that set as JSON. All three are ordinary escaped attributes, so | |
| 14 | - | // nothing here is a program built out of app text -- which is why the behaviour | |
| 15 | - | // is a script rather than one of the emitted _hyperscript programs, whose one | |
| 16 | - | // rule is that no program is written from text a user typed. | |
| 17 | - | // | |
| 18 | - | // A page that does not serve this file shows every conditional region, which is | |
| 19 | - | // the direction everything else in this renderer degrades in: more content | |
| 20 | - | // rather than less. The six hand-written toggles this replaces fail the other | |
| 21 | - | // way, since their markup starts hidden. | |
| 22 | - | (() => { | |
| 23 | - | "use strict"; | |
| 24 | - | ||
| 25 | - | /** The control a region watches. */ | |
| 26 | - | const CONTROL = "data-reveal"; | |
| 27 | - | /** What that control has to hold: `any`, `none` or `value`. */ | |
| 28 | - | const WHEN = "data-reveal-when"; | |
| 29 | - | /** The values `value` accepts, as a JSON array. */ | |
| 30 | - | const VALUES = "data-reveal-values"; | |
| 31 | - | ||
| 32 | - | /** | |
| 33 | - | * What the named control is holding now, or null when it holds nothing. | |
| 34 | - | * | |
| 35 | - | * By `name` rather than by `id`, because `name` is what the description | |
| 36 | - | * carries: it is what a submit sends the value under and what a region | |
| 37 | - | * names. An id is scoped per form instance by the emitter and is therefore | |
| 38 | - | * not the string the description wrote. | |
| 39 | - | * | |
| 40 | - | * Searched from the region outward: the nearest enclosing form first, then | |
| 41 | - | * the document, which is `fill.js`'s rule and is here for the same reason. | |
| 42 | - | * A screen showing the same form twice -- an edit modal over a list -- | |
| 43 | - | * otherwise reads the first copy in the document whichever one the reader | |
| 44 | - | * is in. | |
| 45 | - | * | |
| 46 | - | * A tick is there by presence, the way a form submits one, so an unticked | |
| 47 | - | * box and an empty box are one answer. That is the convention the router | |
| 48 | - | * states and the other two renderers hold. | |
| 49 | - | */ | |
| 50 | - | const held = (region, name) => { | |
| 51 | - | const selector = `[name="${CSS.escape(name)}"]`; | |
| 52 | - | const scope = region.closest("form") ?? document; | |
| 53 | - | const controls = scope.querySelectorAll(selector); | |
| 54 | - | let value = null; | |
| 55 | - | for (const control of controls) { | |
| 56 | - | if (control.type === "checkbox" || control.type === "radio") { | |
| 57 | - | if (control.checked) { | |
| 58 | - | value = control.value === "" ? "on" : control.value; | |
| 59 | - | } | |
| 60 | - | continue; | |
| 61 | - | } | |
| 62 | - | value = control.value ?? null; | |
| 63 | - | } | |
| 64 | - | return value === "" ? null : value; | |
| 65 | - | }; | |
| 66 | - | ||
| 67 | - | /** Whether a region asking for this is satisfied by what is held. */ | |
| 68 | - | const satisfied = (region, value) => { | |
| 69 | - | const when = region.getAttribute(WHEN); | |
| 70 | - | if (when === "any") { | |
| 71 | - | return value !== null; | |
| 72 | - | } | |
| 73 | - | if (when === "none") { | |
| 74 | - | return value === null; | |
| 75 | - | } | |
| 76 | - | if (value === null) { | |
| 77 | - | return false; | |
| 78 | - | } | |
| 79 | - | let wanted; | |
| 80 | - | try { | |
| 81 | - | wanted = JSON.parse(region.getAttribute(VALUES) ?? "[]"); | |
| 82 | - | } catch { | |
| 83 | - | // An attribute this file did not write. Nothing is revealed on a | |
| 84 | - | // condition nobody can read, which is the same answer a region | |
| 85 | - | // watching a control that is not on the screen gets. | |
| 86 | - | return false; | |
| 87 | - | } | |
| 88 | - | return Array.isArray(wanted) && wanted.includes(value); | |
| 89 | - | }; | |
| 90 | - | ||
| 91 | - | /** | |
| 92 | - | * Put every conditional region where its condition says it belongs. | |
| 93 | - | * | |
| 94 | - | * A walk over the document rather than over a list captured when the page | |
| 95 | - | * loaded, for the reason the clock gives: a swap brings new regions and | |
| 96 | - | * takes old ones away, and both are found by the next pass. | |
| 97 | - | */ | |
| 98 | - | const settle = () => { | |
| 99 | - | for (const region of document.querySelectorAll(`[${CONTROL}]`)) { | |
| 100 | - | const value = held(region, region.getAttribute(CONTROL) ?? ""); | |
| 101 | - | region.hidden = !satisfied(region, value); | |
| 102 | - | } | |
| 103 | - | }; | |
| 104 | - | ||
| 105 | - | // Delegated, and on both events a control settles under: `change` is what a | |
| 106 | - | // checkbox and a select fire, `input` is what a box being typed into fires, | |
| 107 | - | // and a region may watch either. | |
| 108 | - | document.addEventListener("change", settle); | |
| 109 | - | document.addEventListener("input", settle); | |
| 110 | - | ||
| 111 | - | // A region that arrives mid-page should not wait for the reader to touch | |
| 112 | - | // something before it decides whether it applies. htmx's event and the | |
| 113 | - | // initial parse both land here. | |
| 114 | - | // | |
| 115 | - | // `htmx:after:settle` is htmx 4's name for it; 2.x spelled the same event | |
| 116 | - | // `htmx:afterSettle`, and 4 renamed every event to phase:action. | |
| 117 | - | document.addEventListener("htmx:after:settle", settle); | |
| 118 | - | if (document.readyState === "loading") { | |
| 119 | - | document.addEventListener("DOMContentLoaded", settle); | |
| 120 | - | } else { | |
| 121 | - | settle(); | |
| 122 | - | } | |
| 123 | - | })(); |