/** * @fileoverview Delegated event dispatch, the CSP-safe replacement for inline * `on*` HTML attributes. * * Tauri v2 injects a nonce into the CSP `script-src`, which makes * `'unsafe-inline'` ignored, so inline `onclick="..."` handlers are refused by * a compliant WebView (WebKitGTK on Linux). This module registers one delegated * listener per event type on `document`; markup opts in with data attributes. * * Convention, replace: * onclick="GoingsOn.tasks.open('123')" * with: * data-act="tasks.open" data-a1="123" * * - `data-act` (click), `data-change`, `data-input`, `data-submit`, * `data-keydown`, `data-contextmenu`, `data-mousedown` hold a dot-path * resolved against `window.GoingsOn`. * - Positional args come from `data-a1`, `data-a2`, ... (strings), or a single * `data-args` holding a JSON array (for typed / null / mixed args; string * elements may reference the data-a* values via `@a1`, `@a2`, ...). * - Sentinel arg values, substituted before the call, preserve existing method * signatures that took `event`/`this`: * `@event` → the DOM event, `@el` → the target element, * `@value` → the element's `.value`, `@aN` → `data-aN`. * So `onclick="ctx.showTask(event, '${id}')"` becomes * `data-act="ctx.showTask" data-a1="@event" data-a2="123"` with no change to * `showTask(event, id)`. * - `change` / `input` with *no* explicit args pass the element's value: * `data-change="foo"` → `foo(el.value)`, matching `onchange="foo(this.value)"`. * - Nested clickables: the innermost `[data-act]` wins via `closest()`, so a row * and a button inside it can both carry `data-act` and only the button fires, * no `event.stopPropagation()` needed. * - The dispatcher never appends its own context argument, so a method with * optional trailing params is never surprised by an extra value. */ (function () { 'use strict'; /** Resolve a dot-path (e.g. "tasks.open") to a function on GoingsOn. */ function resolve(path) { if (!path) return null; var parts = path.split('.'); var obj = window.GoingsOn; for (var i = 0; i < parts.length && obj != null; i++) { obj = obj[parts[i]]; } return typeof obj === 'function' ? obj : null; } /** Substitute a sentinel string with the live event/element value. */ function subst(v, event, el) { if (typeof v !== 'string' || v.charCodeAt(0) !== 64 /* '@' */) return v; if (v === '@event') return event; if (v === '@el') return el; if (v === '@value') return el.value; if (v === '@checked') return el.checked; var m = /^@a(\d+)$/.exec(v); if (m) return el.dataset['a' + m[1]]; return v; } /** Build the argument list for a handler element. */ function argsFrom(el, event, valueFirst) { var raw; if (el.dataset.args !== undefined) { try { raw = JSON.parse(el.dataset.args); } catch (e) { raw = []; } } else { raw = []; var i = 1, v; while ((v = el.dataset['a' + i]) !== undefined) { raw.push(v); i++; } if (raw.length === 0 && valueFirst) return [el.value]; } return raw.map(function (x) { return subst(x, event, el); }); } /** * Register a delegated listener for `eventType`, dispatching elements that * carry `data-`. */ function bind(eventType, attr, opts) { opts = opts || {}; var selector = '[data-' + attr + ']'; document.addEventListener(eventType, function (event) { var start = event.target; if (!start || !start.closest) return; var el = start.closest(selector); if (!el) return; var fn = resolve(el.dataset[attr]); if (!fn) return; if (opts.preventDefault) event.preventDefault(); fn.apply(el, argsFrom(el, event, opts.valueFirst)); }); } bind('click', 'act'); bind('change', 'change', { valueFirst: true }); bind('input', 'input', { valueFirst: true }); bind('submit', 'submit', { preventDefault: true }); bind('keydown', 'keydown'); bind('contextmenu', 'contextmenu'); bind('mousedown', 'mousedown'); bind('dragstart', 'dragstart'); bind('dragover', 'dragover'); bind('drop', 'drop'); // `mouseenter` does not bubble, so delegate via `mouseover` (which does). // Handlers should be idempotent per target element. bind('mouseover', 'mouseenter'); })();