// Folding a branch of an outline, which is the reader tidying their own view. // // `Row::depth` and `Row::open` describe a hierarchy as a flat list of rows, // each saying how deep it sits. Which rows a shut branch hides falls out of // that reading, and the renderer already did it once: every row is in the // document and the ones under a shut branch arrived `hidden`. What is left is // the gesture, and the gesture is a browser's. // // Nothing here asks the app anything. Folding changes what the reader is // looking at and nothing anyone else can observe -- see `Row::open` for why // that is not a write -- so a round trip would be a request for rows the page // is already holding. // // It reads two hooks the renderer emits and nothing else: `[data-disclose]` on // a branch's chevron, and `data-depth` on the rows. A page that never runs this // keeps the outline in the shape the description gave it, with every row still // reachable -- the shut branches simply stay shut. (() => { "use strict"; const DISCLOSE = "[data-disclose]"; const ROW = "[data-row]"; /** How deep a row sits. Absent means top level, which is what it means. */ const depth = (row) => Number(row.getAttribute("data-depth") ?? 0); /** The row a chevron belongs to. */ const rowOf = (el) => el.closest(ROW); /** * The rows under `row`: the ones after it that are deeper, up to the first * one no deeper than it. * * The same reading `quasi_router::folded` makes on the other side, and it * has to be: a reader who folds a branch here and reloads the page must see * the list the server would have folded. */ const under = (row) => { const rows = [...row.parentElement.querySelectorAll(`:scope > ${ROW}`)]; const start = rows.indexOf(row); const level = depth(row); const kin = []; for (const next of rows.slice(start + 1)) { if (depth(next) <= level) break; kin.push(next); } return kin; }; /** * Draw the outline under `row` as its chevrons say it should be. * * Walked rather than toggled row by row: a branch inside a shut branch * keeps its own state and stays shut when the outer one opens, which is * what a reader who left it shut expects to come back to. `folded` is the * same one-variable walk the Rust side makes. */ const settle = (row) => { // Seeded from the row that was just pressed, which is the whole of // `d131a14c`: this started at `null` and only ever learned about a // *descendant's* chevron, so shutting a branch hid nothing at all. // Every row under a shut one is hidden, and the row's own state is the // only thing that says whether it is shut. let shut = row.querySelector(DISCLOSE)?.getAttribute("aria-expanded") === "false" ? depth(row) : null; for (const next of under(row)) { const level = depth(next); if (shut !== null && level <= shut) shut = null; const hidden = shut !== null; next.hidden = hidden; const chevron = next.querySelector(DISCLOSE); if (!hidden && chevron?.getAttribute("aria-expanded") === "false") { shut = level; } } }; // Capture, so this runs before anything bound on the way down. // // Found 2026-08-30 by the first test written against this file. Bubbling, // `stopPropagation` here stopped nothing that had already run: a listener // on the row is *below* the document in the bubble path, so it fires // first. That is not hypothetical -- `node.rs` hangs a table row's // `activate` on the row element (`Fires::ClickBeside`) and filters it with // `click[!event.target.closest('[data-act]')]`, which a chevron is not, so // pressing the chevron on a row that both discloses and activates folded // the branch *and* navigated away from it. The emitter's own comment // already claimed otherwise: "filters by it, pressing its chevron does // not." // // Fixed here rather than by widening that filter, because this is the one // place that knows what a chevron is. document.addEventListener("click", (event) => { const chevron = event.target.closest(DISCLOSE); if (!chevron) return; const row = rowOf(chevron); if (!row) return; // Never the row's own act. A chevron is a separate hit target from the // label precisely so that pressing it is not pressing the row, and a // row that activates on click would otherwise navigate underneath the // fold. event.preventDefault(); event.stopPropagation(); const open = chevron.getAttribute("aria-expanded") === "true"; chevron.setAttribute("aria-expanded", String(!open)); chevron.setAttribute("aria-label", open ? "Expand" : "Collapse"); settle(row); }, true); })();