| 1 |
// Durations named by what they are waiting for, read from the stylesheet. |
| 2 |
// |
| 3 |
// `build.rs` generates `static/timing.css` from `makeover-timing` and |
| 4 |
// `shell.rs` links it on every page, so the crate's numbers are already on the |
| 5 |
// document as custom properties. Reading them here is what makes the number the |
| 6 |
// crate's: nothing below states a duration except as a fallback for a page |
| 7 |
// whose stylesheet has not applied yet, and each fallback is the crate's |
| 8 |
// current value. Same route `static/quasi-clock.js` already takes. |
| 9 |
// |
| 10 |
// The four waits (Revert, Clear, Dismiss, Debounce) are one axis and |
| 11 |
// `Motion::Fade` is another: how long a state lasts is not how long a change |
| 12 |
// takes. The reduced-motion block in the generated sheet zeroes the second and |
| 13 |
// leaves the first alone, which is why a caller that wants a wait must not |
| 14 |
// reach for the fade. |
| 15 |
|
| 16 |
/** Fallbacks, for the tick before the stylesheet applies. `makeover-timing` |
| 17 |
* 0.1.1: Revert 1500, Clear 2000, Dismiss 3000, Debounce 150, Fade 300. */ |
| 18 |
const FALLBACK = { |
| 19 |
'--timing-revert': 1500, |
| 20 |
'--timing-clear': 2000, |
| 21 |
'--timing-dismiss': 3000, |
| 22 |
'--timing-debounce': 150, |
| 23 |
'--motion-fade': 300, |
| 24 |
} as const; |
| 25 |
|
| 26 |
type Token = keyof typeof FALLBACK; |
| 27 |
|
| 28 |
/** Parsed values, kept once read. A miss is not cached: this module can be |
| 29 |
* asked for a duration before the sheet it asks about has been applied, and a |
| 30 |
* property that is not there yet reads as the empty string. */ |
| 31 |
const known = new Map<Token, number>(); |
| 32 |
|
| 33 |
/** A CSS time as milliseconds. `2000ms` and `2s` are both legal spellings of |
| 34 |
* the same duration and a stylesheet may carry either, so the unit is read |
| 35 |
* rather than assumed. */ |
| 36 |
function read(token: Token): number { |
| 37 |
const cached = known.get(token); |
| 38 |
if (cached !== undefined) return cached; |
| 39 |
// No document to ask: this module is imported by `node --test` as well as by |
| 40 |
// a page, and a test asserting on the helpers should not have to stand up a |
| 41 |
// DOM to reach them. |
| 42 |
if (typeof getComputedStyle !== 'function') return FALLBACK[token]; |
| 43 |
const raw = getComputedStyle(document.documentElement).getPropertyValue(token).trim(); |
| 44 |
const parsed = Number.parseFloat(raw); |
| 45 |
if (!Number.isFinite(parsed)) return FALLBACK[token]; |
| 46 |
const value = raw.endsWith('ms') ? parsed : parsed * 1000; |
| 47 |
known.set(token, value); |
| 48 |
return value; |
| 49 |
} |
| 50 |
|
| 51 |
/** How long a temporary label stays before the real one comes back. */ |
| 52 |
export function revertMs(): number { |
| 53 |
return read('--timing-revert'); |
| 54 |
} |
| 55 |
|
| 56 |
/** How long a line of feedback stays before it clears itself. */ |
| 57 |
export function clearMs(): number { |
| 58 |
return read('--timing-clear'); |
| 59 |
} |
| 60 |
|
| 61 |
/** How long a notice lives before it starts to leave. */ |
| 62 |
export function dismissMs(): number { |
| 63 |
return read('--timing-dismiss'); |
| 64 |
} |
| 65 |
|
| 66 |
/** How long input waits for the typing to stop. */ |
| 67 |
export function debounceMs(): number { |
| 68 |
return read('--timing-debounce'); |
| 69 |
} |
| 70 |
|
| 71 |
/** How long a change takes. Not a wait: reduced motion zeroes this one. */ |
| 72 |
export function fadeMs(): number { |
| 73 |
return read('--motion-fade'); |
| 74 |
} |
| 75 |
|
| 76 |
/** Pending debounced calls, keyed by the caller's name for the input. */ |
| 77 |
const pending = new Map<string, ReturnType<typeof setTimeout>>(); |
| 78 |
|
| 79 |
/** Run `fn` once `key` has been quiet for `ms`, replacing any call still |
| 80 |
* waiting under that key. |
| 81 |
* |
| 82 |
* The key rather than a returned handle, so a caller keeps no timer variable: |
| 83 |
* the hand-rolled `var debounce; clearTimeout(debounce); debounce = |
| 84 |
* setTimeout(…)` this replaces was written eight times across `static/*.js`, |
| 85 |
* each with its own number. |
| 86 |
* |
| 87 |
* `ms` defaults to `Intent::Debounce` and is worth passing only for a wait |
| 88 |
* that is not one -- an autosave measured in tens of seconds is a cadence, not |
| 89 |
* input waiting for the typing to stop. */ |
| 90 |
export function debounce(key: string, fn: () => void, ms = debounceMs()): void { |
| 91 |
cancelDebounce(key); |
| 92 |
pending.set( |
| 93 |
key, |
| 94 |
setTimeout(() => { |
| 95 |
pending.delete(key); |
| 96 |
fn(); |
| 97 |
}, ms), |
| 98 |
); |
| 99 |
} |
| 100 |
|
| 101 |
/** Drop the call waiting under `key`, if any. What an input handler calls when |
| 102 |
* the field went empty and the request it was going to make is moot. */ |
| 103 |
export function cancelDebounce(key: string): void { |
| 104 |
const timer = pending.get(key); |
| 105 |
if (timer !== undefined) clearTimeout(timer); |
| 106 |
pending.delete(key); |
| 107 |
} |
| 108 |
|
| 109 |
/** Empty `el` after `Intent::Clear`, so a "Saved" line takes itself away. |
| 110 |
* |
| 111 |
* `expected` guards the clear against a later message: an autosave that |
| 112 |
* reports again while the first line is still up should not have its new text |
| 113 |
* wiped by the old timer. Omitted, the line clears unconditionally. */ |
| 114 |
export function clearStatusLater(el: HTMLElement, expected?: string): void { |
| 115 |
setTimeout(() => { |
| 116 |
if (expected === undefined || el.textContent === expected) el.textContent = ''; |
| 117 |
}, clearMs()); |
| 118 |
} |
| 119 |
|
| 120 |
/** What `installLegacyBridge` puts on `window` for the not-yet-migrated |
| 121 |
* `static/*.js` files. One object rather than a name each: the globals ratchet |
| 122 |
* counts names, and a namespace that grows costs nothing. */ |
| 123 |
export const timing = { |
| 124 |
revertMs, |
| 125 |
clearMs, |
| 126 |
dismissMs, |
| 127 |
debounceMs, |
| 128 |
fadeMs, |
| 129 |
debounce, |
| 130 |
cancelDebounce, |
| 131 |
clearStatusLater, |
| 132 |
}; |
| 133 |
|