// Durations named by what they are waiting for, read from the stylesheet. // // `build.rs` generates `static/timing.css` from `makeover-timing` and // `shell.rs` links it on every page, so the crate's numbers are already on the // document as custom properties. Reading them here is what makes the number the // crate's: nothing below states a duration except as a fallback for a page // whose stylesheet has not applied yet, and each fallback is the crate's // current value. Same route `static/quasi-clock.js` already takes. // // The four waits (Revert, Clear, Dismiss, Debounce) are one axis and // `Motion::Fade` is another: how long a state lasts is not how long a change // takes. The reduced-motion block in the generated sheet zeroes the second and // leaves the first alone, which is why a caller that wants a wait must not // reach for the fade. /** Fallbacks, for the tick before the stylesheet applies. `makeover-timing` * 0.1.1: Revert 1500, Clear 2000, Dismiss 3000, Debounce 150, Fade 300. */ const FALLBACK = { '--timing-revert': 1500, '--timing-clear': 2000, '--timing-dismiss': 3000, '--timing-debounce': 150, '--motion-fade': 300, } as const; type Token = keyof typeof FALLBACK; /** Parsed values, kept once read. A miss is not cached: this module can be * asked for a duration before the sheet it asks about has been applied, and a * property that is not there yet reads as the empty string. */ const known = new Map(); /** A CSS time as milliseconds. `2000ms` and `2s` are both legal spellings of * the same duration and a stylesheet may carry either, so the unit is read * rather than assumed. */ function read(token: Token): number { const cached = known.get(token); if (cached !== undefined) return cached; // No document to ask: this module is imported by `node --test` as well as by // a page, and a test asserting on the helpers should not have to stand up a // DOM to reach them. if (typeof getComputedStyle !== 'function') return FALLBACK[token]; const raw = getComputedStyle(document.documentElement).getPropertyValue(token).trim(); const parsed = Number.parseFloat(raw); if (!Number.isFinite(parsed)) return FALLBACK[token]; const value = raw.endsWith('ms') ? parsed : parsed * 1000; known.set(token, value); return value; } /** How long a temporary label stays before the real one comes back. */ export function revertMs(): number { return read('--timing-revert'); } /** How long a line of feedback stays before it clears itself. */ export function clearMs(): number { return read('--timing-clear'); } /** How long a notice lives before it starts to leave. */ export function dismissMs(): number { return read('--timing-dismiss'); } /** How long input waits for the typing to stop. */ export function debounceMs(): number { return read('--timing-debounce'); } /** How long a change takes. Not a wait: reduced motion zeroes this one. */ export function fadeMs(): number { return read('--motion-fade'); } /** Pending debounced calls, keyed by the caller's name for the input. */ const pending = new Map>(); /** Run `fn` once `key` has been quiet for `ms`, replacing any call still * waiting under that key. * * The key rather than a returned handle, so a caller keeps no timer variable: * the hand-rolled `var debounce; clearTimeout(debounce); debounce = * setTimeout(…)` this replaces was written eight times across `static/*.js`, * each with its own number. * * `ms` defaults to `Intent::Debounce` and is worth passing only for a wait * that is not one -- an autosave measured in tens of seconds is a cadence, not * input waiting for the typing to stop. */ export function debounce(key: string, fn: () => void, ms = debounceMs()): void { cancelDebounce(key); pending.set( key, setTimeout(() => { pending.delete(key); fn(); }, ms), ); } /** Drop the call waiting under `key`, if any. What an input handler calls when * the field went empty and the request it was going to make is moot. */ export function cancelDebounce(key: string): void { const timer = pending.get(key); if (timer !== undefined) clearTimeout(timer); pending.delete(key); } /** Empty `el` after `Intent::Clear`, so a "Saved" line takes itself away. * * `expected` guards the clear against a later message: an autosave that * reports again while the first line is still up should not have its new text * wiped by the old timer. Omitted, the line clears unconditionally. */ export function clearStatusLater(el: HTMLElement, expected?: string): void { setTimeout(() => { if (expected === undefined || el.textContent === expected) el.textContent = ''; }, clearMs()); } /** What `installLegacyBridge` puts on `window` for the not-yet-migrated * `static/*.js` files. One object rather than a name each: the globals ratchet * counts names, and a namespace that grows costs nothing. */ export const timing = { revertMs, clearMs, dismissMs, debounceMs, fadeMs, debounce, cancelDebounce, clearStatusLater, };