Skip to main content

max / makenotwork

5.0 KB · 133 lines History Blame Raw
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