max / quasi
| 1 | // What a browser knows about the time and a description cannot. |
| 2 | // |
| 3 | // `f00244a6`: the renderer owns the clock. A description carries an instant and |
| 4 | // which way a readout runs against now, and picking the words and the rate is |
| 5 | // the renderer's half. In this renderer the markup is emitted once by a server |
| 6 | // and then held by a browser for as long as the reader is on the page, so this |
| 7 | // file is where every set of words after the first one is made. |
| 8 | // |
| 9 | // The first set is Rust's, in `clock.rs`, and the two formats have to agree or |
| 10 | // the page changes what it says a second after it loads. Both sides carry a |
| 11 | // note saying so and both are covered by tests. |
| 12 | // |
| 13 | // It also takes toasts away, which is the other thing on a page that happens |
| 14 | // because time passed and for which the browser is the only one awake. |
| 15 | // `4453bf82`: the description says a notice goes away on its own and declines |
| 16 | // to say when, so the when is the renderer's, and in this renderer that means |
| 17 | // here. It rides in this file rather than in a second script the host has to |
| 18 | // serve, because it is the same fact -- what a browser knows about the time and |
| 19 | // a description cannot -- and a page dropping the clock has no timer left to |
| 20 | // take a toast away with either. |
| 21 | // |
| 22 | // Nothing here knows what a readout is of. It reads two attributes the renderer |
| 23 | // emits, `data-clock` for which way the readout runs and `data-at` for the |
| 24 | // instant in milliseconds, and it is the whole of the per-app elapsed-time |
| 25 | // plumbing this stack exists to delete. The apps that hand-write it run to |
| 26 | // hundreds of lines each. |
| 27 | |
| 28 | "use strict"; |
| 29 | |
| 30 | // Two rates, because the cadence follows the granularity the words are |
| 31 | // shown at. A stopwatch reads seconds and needs one; a stamp reads "3h ago" |
| 32 | // and changes on the minute at its finest, so waking for it every second |
| 33 | // would repaint the page 60 times to change nothing. |
| 34 | // |
| 35 | // Many readouts, one tick: every readout of a kind is written by that |
| 36 | // kind's own interval, rather than each element carrying a timer. A page of |
| 37 | // running timers costs one wake a second however many of them there are, |
| 38 | // which is what the hand-written version arrives at after it has been |
| 39 | // rewritten twice. |
| 40 | const RATES = ; |
| 41 | |
| 42 | // How long a toast stays before it is taken off the page. |
| 43 | // |
| 44 | // `eea7ba88`. Read rather than written: `makeover-timing` names the |
| 45 | // duration `Intent::Dismiss`, and `makeover-build` writes it into every |
| 46 | // consumer's `timing.css` as `--timing-dismiss`. The terminal and egui |
| 47 | // renderers take the same number out of the same crate, so a screen |
| 48 | // described once and drawn three times keeps its messages for one length of |
| 49 | // time and moving that length is moving one constant. |
| 50 | // |
| 51 | // The fallback is for a page that ships this script without that |
| 52 | // stylesheet. It is the crate's own value and a Rust test pins it there, the |
| 53 | // same arrangement the two format implementations above already run on: a |
| 54 | // number in two languages is fine so long as something fails when they part. |
| 55 | const LINGER_FALLBACK = 3000; |
| 56 | |
| 57 | // How long a toast takes to leave, once its linger is up. |
| 58 | // |
| 59 | // `43bdbff7`. `makeover-timing` has always said `Intent::Dismiss` is how |
| 60 | // long a notice lives *before it starts to leave*, and that the leaving is |
| 61 | // `Motion::Fade`; the renderers removed the node at Dismiss and the fade |
| 62 | // was emitted into every `timing.css` and read by nothing. It is read here, |
| 63 | // and `makeover-webview` 0.69.0 carries the rule that draws it. |
| 64 | // |
| 65 | // The fallback is `Motion::Fade.ms()`, pinned by a Rust test the same way |
| 66 | // `LINGER_FALLBACK` is: a number in two languages is fine so long as |
| 67 | // something fails when they part. |
| 68 | const FADE_FALLBACK = 300; |
| 69 | |
| 70 | // A CSS time as a number of milliseconds. `3000ms` and `3s` are both legal |
| 71 | // spellings of the same duration and a stylesheet may carry either, so the |
| 72 | // unit is read rather than assumed. An absent or unparseable value is a page |
| 73 | // with no timing sheet, not a page asking for zero. |
| 74 | const |
| 75 | const raw = |
| 76 | . |
| 77 | .; |
| 78 | const parsed = Number.; |
| 79 | return fallback; |
| 80 | return raw. ? parsed : parsed * 1000; |
| 81 | }; |
| 82 | |
| 83 | // Read once, on the first settle rather than at parse time: this script may |
| 84 | // run before the stylesheet it is asking about has been applied, and a |
| 85 | // custom property that is not there yet reads as the empty string. |
| 86 | let lingerMs = null; |
| 87 | let fadeMs = null; |
| 88 | |
| 89 | /** A span in seconds as a stopwatch: `h:mm:ss`, hours unbounded. */ |
| 90 | const |
| 91 | const whole = Math.; |
| 92 | const mm = .; |
| 93 | const ss = .; |
| 94 | return `::`; |
| 95 | }; |
| 96 | |
| 97 | /** A span in seconds as a stamp: the largest unit that is not zero. */ |
| 98 | const |
| 99 | const whole = Math.; |
| 100 | return "just now"; |
| 101 | return `m ago`; |
| 102 | return `h ago`; |
| 103 | return `d ago`; |
| 104 | }; |
| 105 | |
| 106 | /** What one readout says now. */ |
| 107 | const |
| 108 | const seconds = / 1000; |
| 109 | return ; |
| 110 | return ; |
| 111 | return ; |
| 112 | }; |
| 113 | |
| 114 | /** Write every readout of one kind. */ |
| 115 | const |
| 116 | const now = Date.; |
| 117 | |
| 118 | const at = ; |
| 119 | |
| 120 | continue; |
| 121 | |
| 122 | readout. = ; |
| 123 | |
| 124 | }; |
| 125 | |
| 126 | // One interval per kind, started once and never cleared. A swap brings new |
| 127 | // readouts and takes old ones away, and both are found by the next tick |
| 128 | // because the walk is over the document rather than over a list captured |
| 129 | // when the page loaded -- the same reason the selection script delegates |
| 130 | // its events. |
| 131 | |
| 132 | ; |
| 133 | |
| 134 | |
| 135 | // Start the clock on every toast that has not been given one. |
| 136 | // |
| 137 | // A timer per element, which is what the readouts above deliberately do not |
| 138 | // do. The reasoning is the same reasoning and it comes out the other way: a |
| 139 | // page carries many readouts and each one wants waking again and again, and |
| 140 | // it carries one or two toasts whose timer runs once and is over. What a |
| 141 | // shared interval would buy here is a message that leaves up to a second |
| 142 | // late, which is the whole of what the reader would see. |
| 143 | // |
| 144 | // The mark is what keeps a settle from starting a second timer on a toast |
| 145 | // that is already counting. |
| 146 | const |
| 147 | |
| 148 | lingerMs = ; |
| 149 | fadeMs = ; |
| 150 | |
| 151 | const fresh = '[data-notice="toast"]:not([data-lingering])'; |
| 152 | |
| 153 | toast.; |
| 154 | // Two steps, because Dismiss is when the leaving *starts*. The |
| 155 | // attribute is what `makeover-webview`'s rule transitions on; the |
| 156 | // node goes when the transition is over. |
| 157 | |
| 158 | toast.; |
| 159 | // A timer and not `transitionend`, deliberately. `timing.css` |
| 160 | // zeroes `--motion-fade` under `prefers-reduced-motion`, and a |
| 161 | // zero-length transition may fire no event at all -- a node |
| 162 | // waiting on one that never comes would stay on the page |
| 163 | // forever, for exactly the reader who asked for less motion. |
| 164 | // The timer costs one frame there and cannot hang. |
| 165 | ; |
| 166 | }, lingerMs; |
| 167 | |
| 168 | }; |
| 169 | |
| 170 | // A readout that arrives mid-page should not wait out a whole period |
| 171 | // showing what the server said a minute ago, and a toast that arrives in a |
| 172 | // swap is exactly how a toast arrives. htmx's event and the initial parse |
| 173 | // both land here. |
| 174 | const |
| 175 | |
| 176 | ; |
| 177 | |
| 178 | ; |
| 179 | }; |
| 180 | |
| 181 | // `htmx:after:settle` is htmx 4's name for it; 2.x spelled the same |
| 182 | // event `htmx:afterSettle`, and 4 renamed every event to phase:action. |
| 183 | document.; |
| 184 | |
| 185 | document.; |
| 186 | |
| 187 | ; |
| 188 | |
| 189 | }); |
| 190 |