Skip to main content

max / goingson

3.2 KB · 68 lines History Blame Raw
1 // Submitting the moment a reader meant, rather than the words they typed.
2 //
3 // A `datetime-local` box asks for a time the way a person says one -- "the 14th
4 // at half past two" -- and that names a different moment in Denver than it does
5 // in Berlin. A route storing an instant needs the moment, and nothing in HTML
6 // converts one to the other: the box submits the characters. So somebody
7 // converts, and in this renderer that is here.
8 //
9 // It is here rather than above the renderer because the browser is the only
10 // party that knows what "your computer's time zone" means. A description cannot
11 // answer it; a server can only guess; a terminal and an egui host would each
12 // answer it their own way. `makeover_layout::Field::as_instant` is the
13 // description saying it wants the conversion and declining to say how, and this
14 // file is the how for one host.
15 //
16 // It reads one attribute the form renderer writes, `data-instant`, and it is
17 // the whole of the per-app publish-at plumbing this stack exists to delete. The
18 // MNW server carried its own copy under a private `data-config` vocabulary.
19 (() => {
20 "use strict";
21
22 // The mark makeover-webview puts on a datetime input whose description
23 // asked for an instant. Only that kind is marked: a date and a time are
24 // each half a moment and cannot name one on their own.
25 const MARK = "data-instant";
26
27 /**
28 * The absolute instant a local wall-clock string names, as RFC 3339.
29 *
30 * `new Date(v)` reads a bare `YYYY-MM-DDTHH:mm` in the browser's own zone,
31 * which is exactly the reading wanted. A value the browser cannot parse is
32 * left alone rather than replaced with `Invalid Date`: an unparseable value
33 * is a validation failure the route should see and answer, and rewriting it
34 * here would turn a legible error into an unreadable one.
35 */
36 const instant = (local) => {
37 const at = new Date(local);
38 return Number.isNaN(at.getTime()) ? null : at.toISOString();
39 };
40
41 // Rewrite on the way out, on the request htmx is about to make, so nothing
42 // the reader can see changes: the box keeps showing the time they typed and
43 // the wire carries the moment it names.
44 //
45 // The walk is over the submitting element's own marked inputs rather than
46 // over the document, so a page with two forms on it converts the one being
47 // submitted. `htmx:config:request` is htmx 4's name; 2.x spelled it
48 // `htmx:configRequest`.
49 document.body.addEventListener("htmx:config:request", (event) => {
50 const source = event.detail?.ctx?.sourceElement;
51 const body = event.detail?.ctx?.request?.body;
52 if (!source?.querySelectorAll || !body) return;
53
54 const marked =
55 source.matches?.(`[${MARK}]`) === true
56 ? [source]
57 : source.querySelectorAll(`[${MARK}]`);
58 for (const input of marked) {
59 const name = input.getAttribute("name");
60 if (!name) continue;
61 const local = body.get(name);
62 if (typeof local !== "string" || !local) continue;
63 const moment = instant(local);
64 if (moment !== null) body.set(name, moment);
65 }
66 });
67 })();
68