mathly / mathly2 /static /busy.js
3v324v23's picture
Claude Sonnet 5.5
mathly2: written work, rounds, caching, one course for all classes, lesson schedule
0815e01
Raw History Blame Contribute Delete
2.58 kB
// A button whose action takes time shows it. One hook for the whole app: the button (or the form's submit button) that was just pressed is
// remembered, and every request made right after that marks it busy if the answer takes longer than about a third of a second: a spinner, and
// a second press does nothing, until the request ends. Quick actions never flash it. Uploads (a file chooser's label) use `withBusy` directly.
const DELAY = 300; // milliseconds before the button shows it is working
const WINDOW = 600; // a request counts as the button's when it starts within this time of the press
const SKIP = ".ex-choice, .qz-choice, .mx-board, [data-board-toggle]"; // these show their own state
let pressed = null, pressedAt = 0;
const counts = new WeakMap();
function target(event) {
const el = event.target?.closest?.("button, .button, [type=submit]");
return el && !el.closest(SKIP) ? el : null;
}
function mark(el) {
counts.set(el, (counts.get(el) || 0) + 1);
el.classList.add("busy");
el.setAttribute("aria-busy", "true");
}
function unmark(el) {
const left = (counts.get(el) || 1) - 1;
counts.set(el, left);
if (left <= 0) { el.classList.remove("busy"); el.removeAttribute("aria-busy"); }
}
document.addEventListener("click", event => {
const el = target(event);
if (!el) return;
if (el.classList.contains("busy")) { event.preventDefault(); event.stopImmediatePropagation(); return; } // a second press while it works
pressed = el;
pressedAt = performance.now();
}, true);
document.addEventListener("submit", event => {
if (event.submitter?.classList.contains("busy")) { event.preventDefault(); event.stopImmediatePropagation(); return; }
if (event.submitter && !event.submitter.closest(SKIP)) { pressed = event.submitter; pressedAt = performance.now(); }
}, true);
/** Follow a promise for a button: after a third of a second the button shows it is working. Returns the promise. */
export function withBusy(el, promise) {
if (!el) return promise;
const timer = setTimeout(() => { if (el.isConnected) { el.dataset.busyOn = "1"; mark(el); } }, DELAY);
const done = () => { clearTimeout(timer); if (el.dataset.busyOn) { delete el.dataset.busyOn; unmark(el); } };
promise.then(done, done);
return promise;
}
/** Follow a request for the button that was just pressed (if one was pressed a moment ago). Used by `api()`. */
export function track(promise) {
const el = performance.now() - pressedAt < WINDOW && pressed?.isConnected ? pressed : null;
return withBusy(el, promise);
}