// What a browser knows about a selection and a description cannot. // // The ticks on a screen are the host's until something submits them: a route // answers a screen built from the store, and how many boxes the reader has // checked since is not a fact the store has. That is why `Act::over` gathers // the ticks at press time rather than the handler being told up front, and it // is why the two things every bulk bar does -- say how many are selected, and // refuse to act on none -- have to be said here. // // Nothing in this file knows what a row stands for. It reads two hooks the // renderer emits, `.row-select` on a tick and `[data-over]` on a control that // commits one, and it is the whole of the per-app selection plumbing this stack // exists to delete. (() => { "use strict"; const TICK = ".row-select"; const COMMIT = "[data-over]"; // Where a control's own words are kept, so appending a count to them is not // a one-way edit. Read back on every sync rather than the label being // reparsed, which is how "Complete (3) (2)" happens. const LABEL = "data-over-label"; /** Every control inside `root` that acts on the selection. */ const commits = (root) => root.querySelectorAll(COMMIT); /** How many ticks are set, across the whole document. */ const chosen = () => document.querySelectorAll(`${TICK}:checked`).length; /** * A control's own words, remembered the first time they are seen. * * A button carries them as its text. A field group carries none of its own * -- its words are the question inside it -- so it is left alone and only * disabled. */ const words = (control) => { if (!control.hasAttribute(LABEL)) { control.setAttribute(LABEL, control.textContent.trim()); } return control.getAttribute(LABEL); }; /** * Disable the control, or the controls inside it. * * A commit act is a button and takes the attribute itself. A picker over a * selection is a wrapper around a control makeover emitted, so the wrapper * is not the thing a browser can disable and the input inside it is. */ const inert = (control, off) => { const inputs = control.querySelectorAll("input, select, textarea, button"); if (inputs.length === 0) { control.toggleAttribute("disabled", off); control.setAttribute("aria-disabled", String(off)); return; } for (const input of inputs) { input.toggleAttribute("disabled", off); } }; const sync = () => { const count = chosen(); for (const control of commits(document)) { inert(control, count === 0); // Only a control that says what it does gets the number // appended. A picker's words are a question, and a question // with a count after it reads as one more option. if (control.tagName === "BUTTON") { const label = words(control); control.textContent = count === 0 ? label : `${label} (${count})`; } } }; // Delegated, because the rows a tick lives on are replaced wholesale by // every filter, sort and write on the screen. Binding to the boxes // themselves would last until the first swap. document.addEventListener("change", (event) => { if (event.target instanceof Element && event.target.matches(TICK)) { sync(); } }); // A swap brings new rows, and the new rows arrive with whatever the server // said about their ticks -- which after a bulk write is none of them. Both // htmx's event and the initial parse 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", sync); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", sync); } else { sync(); } })();