Skip to main content

max / goingson

4.5 KB · 110 lines History Blame Raw
1 /**
2 * @fileoverview Delegated event dispatch, the CSP-safe replacement for inline
3 * `on*` HTML attributes.
4 *
5 * Tauri v2 injects a nonce into the CSP `script-src`, which makes
6 * `'unsafe-inline'` ignored, so inline `onclick="..."` handlers are refused by
7 * a compliant WebView (WebKitGTK on Linux). This module registers one delegated
8 * listener per event type on `document`; markup opts in with data attributes.
9 *
10 * Convention, replace:
11 * onclick="GoingsOn.tasks.open('123')"
12 * with:
13 * data-act="tasks.open" data-a1="123"
14 *
15 * - `data-act` (click), `data-change`, `data-input`, `data-submit`,
16 * `data-keydown`, `data-contextmenu`, `data-mousedown` hold a dot-path
17 * resolved against `window.GoingsOn`.
18 * - Positional args come from `data-a1`, `data-a2`, ... (strings), or a single
19 * `data-args` holding a JSON array (for typed / null / mixed args; string
20 * elements may reference the data-a* values via `@a1`, `@a2`, ...).
21 * - Sentinel arg values, substituted before the call, preserve existing method
22 * signatures that took `event`/`this`:
23 * `@event` → the DOM event, `@el` → the target element,
24 * `@value` → the element's `.value`, `@aN` → `data-aN`.
25 * So `onclick="ctx.showTask(event, '${id}')"` becomes
26 * `data-act="ctx.showTask" data-a1="@event" data-a2="123"` with no change to
27 * `showTask(event, id)`.
28 * - `change` / `input` with *no* explicit args pass the element's value:
29 * `data-change="foo"` → `foo(el.value)`, matching `onchange="foo(this.value)"`.
30 * - Nested clickables: the innermost `[data-act]` wins via `closest()`, so a row
31 * and a button inside it can both carry `data-act` and only the button fires,
32 * no `event.stopPropagation()` needed.
33 * - The dispatcher never appends its own context argument, so a method with
34 * optional trailing params is never surprised by an extra value.
35 */
36
37 (function () {
38 'use strict';
39
40 /** Resolve a dot-path (e.g. "tasks.open") to a function on GoingsOn. */
41 function resolve(path) {
42 if (!path) return null;
43 var parts = path.split('.');
44 var obj = window.GoingsOn;
45 for (var i = 0; i < parts.length && obj != null; i++) {
46 obj = obj[parts[i]];
47 }
48 return typeof obj === 'function' ? obj : null;
49 }
50
51 /** Substitute a sentinel string with the live event/element value. */
52 function subst(v, event, el) {
53 if (typeof v !== 'string' || v.charCodeAt(0) !== 64 /* '@' */) return v;
54 if (v === '@event') return event;
55 if (v === '@el') return el;
56 if (v === '@value') return el.value;
57 if (v === '@checked') return el.checked;
58 var m = /^@a(\d+)$/.exec(v);
59 if (m) return el.dataset['a' + m[1]];
60 return v;
61 }
62
63 /** Build the argument list for a handler element. */
64 function argsFrom(el, event, valueFirst) {
65 var raw;
66 if (el.dataset.args !== undefined) {
67 try { raw = JSON.parse(el.dataset.args); } catch (e) { raw = []; }
68 } else {
69 raw = [];
70 var i = 1, v;
71 while ((v = el.dataset['a' + i]) !== undefined) { raw.push(v); i++; }
72 if (raw.length === 0 && valueFirst) return [el.value];
73 }
74 return raw.map(function (x) { return subst(x, event, el); });
75 }
76
77 /**
78 * Register a delegated listener for `eventType`, dispatching elements that
79 * carry `data-<attr>`.
80 */
81 function bind(eventType, attr, opts) {
82 opts = opts || {};
83 var selector = '[data-' + attr + ']';
84 document.addEventListener(eventType, function (event) {
85 var start = event.target;
86 if (!start || !start.closest) return;
87 var el = start.closest(selector);
88 if (!el) return;
89 var fn = resolve(el.dataset[attr]);
90 if (!fn) return;
91 if (opts.preventDefault) event.preventDefault();
92 fn.apply(el, argsFrom(el, event, opts.valueFirst));
93 });
94 }
95
96 bind('click', 'act');
97 bind('change', 'change', { valueFirst: true });
98 bind('input', 'input', { valueFirst: true });
99 bind('submit', 'submit', { preventDefault: true });
100 bind('keydown', 'keydown');
101 bind('contextmenu', 'contextmenu');
102 bind('mousedown', 'mousedown');
103 bind('dragstart', 'dragstart');
104 bind('dragover', 'dragover');
105 bind('drop', 'drop');
106 // `mouseenter` does not bubble, so delegate via `mouseover` (which does).
107 // Handlers should be idempotent per target element.
108 bind('mouseover', 'mouseenter');
109 })();
110