// A question answered N times, with the reader adding and removing the slots. // // `60d1753c`, ruled 2026-08-25: a repeating group enters the vocabulary and // submits once. This is the browser's half of the third hard part -- the reader // creates and destroys slots without a round trip on every one. The bytes are // already in the document: the emitter writes a `template` holding one blank // slot, and adding is cloning it. // // A round trip instead would re-render a form the reader is midway through, // which is `a135f898`, and it would ask a route for an empty box. // // It reads four attributes the node emitter writes and nothing else: // `data-repeat` names the question, `data-repeat-label` is what its slots are // called, and `data-repeat-least` / `data-repeat-most` are the floor and the // ceiling. All four are ordinary escaped attributes, so nothing here is a // program built out of app text -- which is why this 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 slot the description // offered, fillable and submittable, with two controls that do nothing. (() => { "use strict"; /** The question a fieldset repeats. */ const NAME = "data-repeat"; /** What one slot of it is called. */ const LABEL = "data-repeat-label"; /** The fewest slots that may stand. */ const LEAST = "data-repeat-least"; /** The most that may stand. Absent means no ceiling. */ const MOST = "data-repeat-most"; /** The box the slots sit in. */ const SLOTS = "data-repeat-slots"; /** One slot, and where in the order it currently sits. */ const AT = "data-repeat-at"; /** The control that adds one. */ const ADD = "data-repeat-add"; /** The control that takes one away. */ const REMOVE = "data-repeat-remove"; /** The attributes a rename has to follow the index through. */ const NAMED = ["name", "id", "for", "aria-describedby", "aria-labelledby"]; /** The group a control inside one belongs to. */ const groupOf = (element) => element.closest(`[${NAME}]`); /** Where a group keeps its slots. */ const boxOf = (group) => group.querySelector(`[${SLOTS}]`); /** The slots standing in a group, in document order. */ const slotsOf = (group) => [ ...(boxOf(group)?.querySelectorAll(`:scope > [${AT}]`) ?? []), ]; /** A number written in an attribute, or a fallback when it is not there. */ const count = (group, attribute, fallback) => { const written = group.getAttribute(attribute); if (written === null) { return fallback; } const value = Number.parseInt(written, 10); return Number.isNaN(value) ? fallback : value; }; /** * Move one slot to a place in the order. * * The index is in the slot's own attribute, in every name it submits under * and in the ordinal a person reads. All three are rewritten together, or a * removed slot leaves the ones after it answering under names nobody asked * for. * * The rename is a replacement of `question[old]` wherever it appears, which * covers the derived ids beside the name itself -- `question[0]-hint`, * `question[0]-error` -- without this having to know what the field * emitter derives. */ const renumber = (slot, question, label, to) => { const from = slot.getAttribute(AT); slot.setAttribute(AT, String(to)); if (from === null) { return; } const was = `${question}[${from}]`; const now = `${question}[${to}]`; if (was !== now) { for (const element of [slot, ...slot.querySelectorAll("*")]) { for (const attribute of NAMED) { const value = element.getAttribute(attribute); if (value !== null && value.includes(was)) { element.setAttribute(attribute, value.split(was).join(now)); } } } } // The visible ordinal, which `Repeat::ordinal` wrote as "