// The half of a wait that only the browser can see. // // The node emitter has written `data-awaiting` since the vocabulary gained // `Awaiting`: `determinate` with a `data-awaiting-amount` beside it, or // `indeterminate` alone. `makeover-webview` 0.60.0 draws both. Neither of them // knows when a wait is actually running, and that is what this file supplies. // // Three things, and nothing else: // // 1. `aria-busy` on the element that made the request, for as long as the // request is in flight. That is what the design system keys the mark off, // and it is a standard attribute rather than a class of ours, so it is // announced as well as drawn. // 2. `--awaiting-share`, for a control whose wait has a measured size, from // bytes actually observed arriving. Never from elapsed time: see below. // 3. Nothing at all for a control that is not marked as awaiting, which is // nearly all of them. // // # Why the attribute alone was not enough // // `data-awaiting` is a fact about the control -- pressing this waits -- and it // is true when the page is painted and stays true. Styling it directly would // put a blinking mark on every awaiting control on the screen from the moment // the document loaded. The running-or-not half is true only between two events, // which makes it the binder's, which is this. // // # What the share may not be // // Rule 1 of wiki `loading-and-progress-standard`, and `Awaiting`'s own docs // before it: what is done over what there is, and never a remaining time, an // arrival time, or a rate extrapolated forward. So the share is set from // `event.loaded` -- bytes the browser says have landed -- and from nothing // else. A control with no readable progress keeps a share of zero and draws an // empty trough, which is the honest picture: the size is known and the delivery // is not. // // The one thing this file must never grow is a timer that advances the share. // A bar walking forward on a stalled transfer is the confidently-wrong drawing // the rule exists to forbid, and it is the easiest thing in the world to add. // // # Degrading // // A page that does not serve this file draws no mark and no bar, and every // control still locks itself through `hx-disable` exactly as before. That is // the direction the rest of this renderer degrades in: less decoration, never // less behaviour. (() => { "use strict"; /** The fact about the control: this call waits. */ const AWAITING = "data-awaiting"; /** The measured payload size, when the description carried one. */ const AMOUNT = "data-awaiting-amount"; /** How much of it has landed, as a share from 0 to 1. */ const SHARE = "--awaiting-share"; /** * The element a request belongs to, or null when it is not our business. * * htmx names the element on the event detail. An event for something that * never described a wait is left alone rather than given one: the emitter * decides which calls are worth marking, and a renderer that marked every * request would be overruling it. */ const marked = (event) => { const element = event.detail?.elt; if (!element || typeof element.hasAttribute !== "function") { return null; } return element.hasAttribute(AWAITING) ? element : null; }; /** Say a wait has started, with nothing delivered yet. */ const begin = (event) => { const element = marked(event); if (!element) { return; } element.setAttribute("aria-busy", "true"); element.style.removeProperty(SHARE); }; /** * Say the wait is over. * * The share is cleared with it. A control pressed twice would otherwise * start its second wait wearing the first one's bar, which is a number * about a payload that has already arrived. */ const end = (event) => { const element = marked(event); if (!element) { return; } element.removeAttribute("aria-busy"); element.style.removeProperty(SHARE); }; /** * What has landed, over what the description said there was. * * `event.loaded` when the browser is counting, against the described * amount rather than against `event.total`. The two usually agree, and * where they do not the description is the one the bar was drawn for: a * server that gzips a response reports a total the screen never described. * * A response with no readable length leaves the share alone, so the trough * stays empty rather than jumping to something invented. */ const progressed = (event) => { const element = marked(event); if (!element || element.getAttribute(AWAITING) !== "determinate") { return; } const amount = Number(element.getAttribute(AMOUNT)); const loaded = Number(event.detail?.loaded); if (!Number.isFinite(amount) || amount <= 0 || !Number.isFinite(loaded)) { return; } element.style.setProperty(SHARE, String(Math.min(loaded / amount, 1))); }; // htmx 4 spells every event `phase:action`; 2.x spelled these // `htmx:beforeRequest`, `htmx:afterRequest` and `htmx:xhr:progress`. // // Delegated on the document rather than bound per control, for the reason // the reveal and repeat scripts give: a swap brings new controls and takes // old ones away, and a listener on the document covers both without a // re-scan. document.addEventListener("htmx:before:request", begin); document.addEventListener("htmx:after:request", end); document.addEventListener("htmx:xhr:progress", progressed); // A request that is aborted or errors out never reaches `after:request` in // every htmx version, and a control left wearing `aria-busy` blinks for the // rest of the page's life. Cheap insurance, and the same handler. document.addEventListener("htmx:abort", end); document.addEventListener("htmx:response:error", end); document.addEventListener("htmx:send:error", end); })();