jssm 5.138.0 → 5.141.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/custom-elements.json +384 -47
- package/dist/cdn/instance.js +477 -101
- package/dist/cdn/viz.js +145 -5
- package/dist/cli/fsl-render.cjs +1 -1
- package/dist/cli/fsl.cjs +1 -1
- package/dist/deno/README.md +7 -7
- package/dist/deno/jssm.js +1 -1
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -1
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm_viz.cjs +1 -1
- package/dist/jssm_viz.iife.cjs +1 -1
- package/dist/jssm_viz.mjs +1 -1
- package/dist/wc/instance.js +477 -101
- package/dist/wc/viz.define.js +3 -3
- package/dist/wc/viz.js +132 -2
- package/package.json +1 -1
package/dist/cdn/instance.js
CHANGED
|
@@ -21327,7 +21327,7 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
21327
21327
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
21328
21328
|
* snapshots so that deserializers can detect version-skew.
|
|
21329
21329
|
*/
|
|
21330
|
-
const version = "5.
|
|
21330
|
+
const version = "5.141.0";
|
|
21331
21331
|
|
|
21332
21332
|
// whargarbl lots of these return arrays could/should be sets
|
|
21333
21333
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
@@ -25070,6 +25070,222 @@ function abstract_everything_hook_step(maybe_hook, hook_args) {
|
|
|
25070
25070
|
}
|
|
25071
25071
|
}
|
|
25072
25072
|
|
|
25073
|
+
/**
|
|
25074
|
+
* Walk a dotted path into a value. Used by the `data.path.to.field`
|
|
25075
|
+
* variant of {@link resolve_binding}. Returns `undefined` whenever the
|
|
25076
|
+
* traversal would dereference a non-object, missing field, or `null` —
|
|
25077
|
+
* matching the natural "missing data" semantics rather than throwing.
|
|
25078
|
+
*
|
|
25079
|
+
* ```typescript
|
|
25080
|
+
* walk_path({ a: { b: 7 } }, 'a.b'); // => 7
|
|
25081
|
+
* walk_path({ a: { b: 7 } }, 'a.c'); // => undefined
|
|
25082
|
+
* walk_path({ a: { b: 7 } }, 'a.b.c'); // => undefined (7 is not an object)
|
|
25083
|
+
* walk_path(undefined, 'a'); // => undefined
|
|
25084
|
+
* walk_path({ a: null }, 'a.b'); // => undefined (null is not an object)
|
|
25085
|
+
* walk_path({ a: 1 }, ''); // => { a: 1 } (empty path = identity)
|
|
25086
|
+
* ```
|
|
25087
|
+
*
|
|
25088
|
+
* @param obj - The root value to traverse.
|
|
25089
|
+
* @param path - Dotted path of property names, e.g. `"a.b.c"`.
|
|
25090
|
+
* @returns The terminal value, or `undefined` if any step fails.
|
|
25091
|
+
*/
|
|
25092
|
+
function walk_path(obj, path) {
|
|
25093
|
+
if (path.length === 0) {
|
|
25094
|
+
return obj;
|
|
25095
|
+
}
|
|
25096
|
+
let cur = obj;
|
|
25097
|
+
for (const part of path.split('.')) {
|
|
25098
|
+
if (cur === null || typeof cur !== 'object') {
|
|
25099
|
+
return undefined;
|
|
25100
|
+
}
|
|
25101
|
+
cur = cur[part];
|
|
25102
|
+
}
|
|
25103
|
+
return cur;
|
|
25104
|
+
}
|
|
25105
|
+
/**
|
|
25106
|
+
* Resolve a `<jssm-bind>` / `data-jssm-bind` expression against a live
|
|
25107
|
+
* machine. Throws on any unknown expression — bindings fail fast at
|
|
25108
|
+
* install time rather than silently producing `undefined` strings in the
|
|
25109
|
+
* DOM.
|
|
25110
|
+
*
|
|
25111
|
+
* Recognized expressions:
|
|
25112
|
+
*
|
|
25113
|
+
* | Expression | Resolves to |
|
|
25114
|
+
* | ---------------- | --------------------------------------------- |
|
|
25115
|
+
* | `data` | `machine.data()` |
|
|
25116
|
+
* | `data.a.b.c` | dotted-path traversal into `machine.data()` |
|
|
25117
|
+
* | `state` | `machine.state()` |
|
|
25118
|
+
* | `terminal` | `machine.is_terminal()` |
|
|
25119
|
+
* | `complete` | `machine.is_complete()` |
|
|
25120
|
+
* | `legal-actions` | `machine.list_exit_actions().join(' ')` |
|
|
25121
|
+
*
|
|
25122
|
+
* ```typescript
|
|
25123
|
+
* resolve_binding(m, 'state'); // current state name
|
|
25124
|
+
* resolve_binding(m, 'data.username'); // typed-data subfield
|
|
25125
|
+
* resolve_binding(m, 'wat'); // throws
|
|
25126
|
+
* ```
|
|
25127
|
+
*
|
|
25128
|
+
* @param m - The machine whose state/data is being projected.
|
|
25129
|
+
* @param expr - The binding expression text (raw attribute value).
|
|
25130
|
+
* @returns The resolved value, typed `unknown` since each expression
|
|
25131
|
+
* yields a different shape.
|
|
25132
|
+
*
|
|
25133
|
+
* @throws Error - When `expr` is not a recognized binding form.
|
|
25134
|
+
*/
|
|
25135
|
+
function resolve_binding(m, expr) {
|
|
25136
|
+
switch (expr) {
|
|
25137
|
+
case 'state': return m.state();
|
|
25138
|
+
case 'terminal': return m.is_terminal();
|
|
25139
|
+
case 'complete': return m.is_complete();
|
|
25140
|
+
case 'legal-actions': return m.list_exit_actions().map(a => String(a)).join(' ');
|
|
25141
|
+
case 'data': return m.data();
|
|
25142
|
+
default:
|
|
25143
|
+
if (expr.startsWith('data.')) {
|
|
25144
|
+
return walk_path(m.data(), expr.slice(5));
|
|
25145
|
+
}
|
|
25146
|
+
throw new Error(`<jssm-bind>: unknown binding expression "${expr}"`);
|
|
25147
|
+
}
|
|
25148
|
+
}
|
|
25149
|
+
/**
|
|
25150
|
+
* Apply a resolved binding value to an element's target property. The
|
|
25151
|
+
* `target` selector follows the rules documented in #645:
|
|
25152
|
+
*
|
|
25153
|
+
* - `textContent` (or omitted) sets `el.textContent` to the value coerced
|
|
25154
|
+
* with `String()`.
|
|
25155
|
+
* - Any string starting with `data-` is treated as an attribute name and
|
|
25156
|
+
* set via `setAttribute`, value coerced with `String()`.
|
|
25157
|
+
* - Any other string is assigned directly as a property of the element
|
|
25158
|
+
* (no coercion) — supports `value`, `disabled`, `hidden`, `checked`,
|
|
25159
|
+
* and the documented power-user escape hatch.
|
|
25160
|
+
*
|
|
25161
|
+
* ```typescript
|
|
25162
|
+
* set_on_element(span, 'textContent', 7); // span.textContent = '7'
|
|
25163
|
+
* set_on_element(input, 'value', 'hi'); // input.value = 'hi'
|
|
25164
|
+
* set_on_element(button, 'disabled', true); // button.disabled = true
|
|
25165
|
+
* set_on_element(div, 'data-current', 'red'); // setAttribute('data-current', 'red')
|
|
25166
|
+
* ```
|
|
25167
|
+
*
|
|
25168
|
+
* @param el - The element to update.
|
|
25169
|
+
* @param target - Target property name, possibly a `data-*` attribute.
|
|
25170
|
+
* @param value - The resolved value to assign.
|
|
25171
|
+
*/
|
|
25172
|
+
function set_on_element(el, target, value) {
|
|
25173
|
+
if (target.startsWith('data-')) {
|
|
25174
|
+
el.setAttribute(target, String(value));
|
|
25175
|
+
}
|
|
25176
|
+
else if (target === 'textContent') {
|
|
25177
|
+
el.textContent = String(value);
|
|
25178
|
+
}
|
|
25179
|
+
else {
|
|
25180
|
+
// Power-user escape hatch — assigns value as-is so booleans hit
|
|
25181
|
+
// properties like `disabled`/`hidden`/`checked` with the correct
|
|
25182
|
+
// semantics rather than being coerced to a string.
|
|
25183
|
+
el[target] = value;
|
|
25184
|
+
}
|
|
25185
|
+
}
|
|
25186
|
+
/**
|
|
25187
|
+
* Discover every binding declaration under `host` and install live
|
|
25188
|
+
* subscriptions that refresh them on every machine transition. Returns
|
|
25189
|
+
* a list of unsubscribe callbacks so the host's `disconnectedCallback`
|
|
25190
|
+
* can tear them all down.
|
|
25191
|
+
*
|
|
25192
|
+
* Two surface forms are recognized:
|
|
25193
|
+
*
|
|
25194
|
+
* 1. Inline attribute — any descendant with `data-jssm-bind="<expr>"`.
|
|
25195
|
+
* Optional `data-jssm-bind-to="<target>"` chooses the target property
|
|
25196
|
+
* (defaults to `textContent`).
|
|
25197
|
+
*
|
|
25198
|
+
* 2. Dedicated tag — direct-child `<jssm-bind>` configuration tags with
|
|
25199
|
+
* `selector="<css>"` and `source="<expr>"` attributes, plus an
|
|
25200
|
+
* optional `target="<target>"` (also defaulting to `textContent`).
|
|
25201
|
+
* The `selector` is scoped to `host`'s descendants.
|
|
25202
|
+
*
|
|
25203
|
+
* Each binding is painted once immediately (using the machine's current
|
|
25204
|
+
* state) and then re-painted on every `transition` event.
|
|
25205
|
+
*
|
|
25206
|
+
* ```typescript
|
|
25207
|
+
* // typical install during <jssm-instance>.connectedCallback:
|
|
25208
|
+
* const unsubs = install_bindings(this, this.machine);
|
|
25209
|
+
* this._unsubs.push(...unsubs);
|
|
25210
|
+
* ```
|
|
25211
|
+
*
|
|
25212
|
+
* @param host - The host element whose descendants carry the bindings.
|
|
25213
|
+
* @param machine - The machine whose state/data is being projected.
|
|
25214
|
+
* @returns A flat array of unsubscribe callbacks, one per installed
|
|
25215
|
+
* subscription.
|
|
25216
|
+
*
|
|
25217
|
+
* @throws Error - When any binding expression is unrecognized
|
|
25218
|
+
* (propagated from {@link resolve_binding}).
|
|
25219
|
+
* @throws Error - When a `<jssm-bind>` tag is missing its `selector`
|
|
25220
|
+
* or `source` attribute.
|
|
25221
|
+
*/
|
|
25222
|
+
function install_bindings(host, machine) {
|
|
25223
|
+
var _a, _b;
|
|
25224
|
+
const unsubs = [];
|
|
25225
|
+
// Form 1: inline `data-jssm-bind` on descendants.
|
|
25226
|
+
const inline_nodes = host.querySelectorAll('[data-jssm-bind]');
|
|
25227
|
+
for (const el of Array.from(inline_nodes)) {
|
|
25228
|
+
const expr = el.dataset.jssmBind;
|
|
25229
|
+
const target = (_a = el.dataset.jssmBindTo) !== null && _a !== void 0 ? _a : 'textContent';
|
|
25230
|
+
const apply = () => {
|
|
25231
|
+
set_on_element(el, target, resolve_binding(machine, expr));
|
|
25232
|
+
};
|
|
25233
|
+
apply();
|
|
25234
|
+
unsubs.push(machine.on('transition', apply));
|
|
25235
|
+
}
|
|
25236
|
+
// Form 2: dedicated `<jssm-bind>` configuration tags. Only direct
|
|
25237
|
+
// children are considered configuration tags for THIS host — nested
|
|
25238
|
+
// `<jssm-instance>` children would have their own bindings handled by
|
|
25239
|
+
// their own component.
|
|
25240
|
+
const config_tags = host.querySelectorAll(':scope > jssm-bind');
|
|
25241
|
+
for (const tag of Array.from(config_tags)) {
|
|
25242
|
+
const selector = tag.getAttribute('selector');
|
|
25243
|
+
const expr = tag.getAttribute('source');
|
|
25244
|
+
const target = (_b = tag.getAttribute('target')) !== null && _b !== void 0 ? _b : 'textContent';
|
|
25245
|
+
if (selector === null || selector.length === 0) {
|
|
25246
|
+
throw new Error('<jssm-bind>: missing required "selector" attribute');
|
|
25247
|
+
}
|
|
25248
|
+
if (expr === null || expr.length === 0) {
|
|
25249
|
+
throw new Error('<jssm-bind>: missing required "source" attribute');
|
|
25250
|
+
}
|
|
25251
|
+
const targets = host.querySelectorAll(selector);
|
|
25252
|
+
for (const el of Array.from(targets)) {
|
|
25253
|
+
const apply = () => {
|
|
25254
|
+
set_on_element(el, target, resolve_binding(machine, expr));
|
|
25255
|
+
};
|
|
25256
|
+
apply();
|
|
25257
|
+
unsubs.push(machine.on('transition', apply));
|
|
25258
|
+
}
|
|
25259
|
+
}
|
|
25260
|
+
return unsubs;
|
|
25261
|
+
}
|
|
25262
|
+
/**
|
|
25263
|
+
* `<jssm-bind>` configuration tag. The element itself is invisible —
|
|
25264
|
+
* it carries `selector`, `source`, and optional `target` attributes
|
|
25265
|
+
* that the parent `<jssm-instance>` reads during its connection
|
|
25266
|
+
* lifecycle to wire up a machine-to-DOM binding.
|
|
25267
|
+
*
|
|
25268
|
+
* Registering it as a `LitElement` (rather than leaving it as a generic
|
|
25269
|
+
* unknown tag) gives it a stable upgrade timing, a `display: none`
|
|
25270
|
+
* default style, and a proper place in the custom-elements registry so
|
|
25271
|
+
* `customElements.get('jssm-bind')` resolves.
|
|
25272
|
+
*
|
|
25273
|
+
* @element jssm-bind
|
|
25274
|
+
* @attribute selector - CSS selector for the target element(s), scoped to the host.
|
|
25275
|
+
* @attribute source - Binding expression (see {@link resolve_binding}).
|
|
25276
|
+
* @attribute target - Target property name; defaults to `textContent`.
|
|
25277
|
+
*/
|
|
25278
|
+
class JssmBind extends i {
|
|
25279
|
+
/**
|
|
25280
|
+
* No-op render. The tag's purpose is purely declarative
|
|
25281
|
+
* configuration; it must not contribute any DOM to the page.
|
|
25282
|
+
*/
|
|
25283
|
+
render() {
|
|
25284
|
+
return null;
|
|
25285
|
+
}
|
|
25286
|
+
}
|
|
25287
|
+
JssmBind.styles = i$3 `:host { display: none; }`;
|
|
25288
|
+
|
|
25073
25289
|
const VALID_KINDS = new Set([
|
|
25074
25290
|
'hook',
|
|
25075
25291
|
'named',
|
|
@@ -25133,7 +25349,7 @@ function make_hook_proxy(ctx, machine) {
|
|
|
25133
25349
|
* @param debug_id - Identifier appended to the synthetic sourceURL.
|
|
25134
25350
|
* @returns The compiled handler.
|
|
25135
25351
|
*/
|
|
25136
|
-
function compile_inline_body(body, debug_id) {
|
|
25352
|
+
function compile_inline_body$1(body, debug_id) {
|
|
25137
25353
|
const annotated = `//# sourceURL=jssm-hook:${debug_id}\n${body}`;
|
|
25138
25354
|
const ctor = Function;
|
|
25139
25355
|
return new ctor('m', annotated);
|
|
@@ -25148,7 +25364,7 @@ function compile_inline_body(body, debug_id) {
|
|
|
25148
25364
|
* @returns The resolved handler.
|
|
25149
25365
|
* @throws Error - If no callable of that name is found in either location.
|
|
25150
25366
|
*/
|
|
25151
|
-
function resolve_named_handler(name, registry) {
|
|
25367
|
+
function resolve_named_handler$1(name, registry) {
|
|
25152
25368
|
if (registry !== undefined) {
|
|
25153
25369
|
const registered = registry.get(name);
|
|
25154
25370
|
if (registered !== undefined) {
|
|
@@ -25208,8 +25424,8 @@ function parse_hook_element(el, debug_id, registry) {
|
|
|
25208
25424
|
throw new Error('<jssm-hook>: must specify either handler="name" attribute or an inline body');
|
|
25209
25425
|
}
|
|
25210
25426
|
const user_handler = handler_attr !== null
|
|
25211
|
-
? resolve_named_handler(handler_attr, registry)
|
|
25212
|
-
: compile_inline_body(body_text, debug_id);
|
|
25427
|
+
? resolve_named_handler$1(handler_attr, registry)
|
|
25428
|
+
: compile_inline_body$1(body_text, debug_id);
|
|
25213
25429
|
const kind = normalize_hook_kind(el.getAttribute('kind'));
|
|
25214
25430
|
// Convert null → undefined so downstream descriptors omit absent keys.
|
|
25215
25431
|
const from = (_a = el.getAttribute('from')) !== null && _a !== void 0 ? _a : undefined;
|
|
@@ -25272,6 +25488,177 @@ function build_hook_descriptor(spec, wrapped) {
|
|
|
25272
25488
|
return base;
|
|
25273
25489
|
}
|
|
25274
25490
|
|
|
25491
|
+
/**
|
|
25492
|
+
* Allow-list of event names accepted by `<jssm-on event="...">`. Must stay
|
|
25493
|
+
* in sync with the `JssmEventName` union in `jssm_types.ts` (the library's
|
|
25494
|
+
* `machine.on(...)` event API, added in #638). Validating here gives the
|
|
25495
|
+
* declarative wiring a clear "unknown event name" error at the WC layer
|
|
25496
|
+
* instead of relying on a downstream library throw whose message would
|
|
25497
|
+
* mention `machine.on(...)` rather than the offending tag.
|
|
25498
|
+
*/
|
|
25499
|
+
const JSSM_ON_EVENT_NAMES = new Set([
|
|
25500
|
+
'transition',
|
|
25501
|
+
'rejection',
|
|
25502
|
+
'action',
|
|
25503
|
+
'entry',
|
|
25504
|
+
'exit',
|
|
25505
|
+
'terminal',
|
|
25506
|
+
'complete',
|
|
25507
|
+
'error',
|
|
25508
|
+
'data-change',
|
|
25509
|
+
'override',
|
|
25510
|
+
'timeout',
|
|
25511
|
+
'hook-registration',
|
|
25512
|
+
'hook-removal'
|
|
25513
|
+
]);
|
|
25514
|
+
/**
|
|
25515
|
+
* Parse a `<jssm-on>` element into a validated {@link ParsedJssmOn}
|
|
25516
|
+
* record. Centralized so the declarative-tag logic is testable without
|
|
25517
|
+
* spinning up the full `<jssm-instance>` lifecycle.
|
|
25518
|
+
*
|
|
25519
|
+
* Validation rules (per #643):
|
|
25520
|
+
* - `event` is required and must be in {@link JSSM_ON_EVENT_NAMES}.
|
|
25521
|
+
* - Either a `handler="name"` attribute or non-empty `textContent`
|
|
25522
|
+
* must be supplied, but not both.
|
|
25523
|
+
* - `state` is only meaningful for `event="entry"` / `event="exit"`;
|
|
25524
|
+
* it's silently ignored on other events.
|
|
25525
|
+
* - `from` / `to` are only meaningful for `event="transition"`; they
|
|
25526
|
+
* are silently ignored on other events. Both → AND (a specific
|
|
25527
|
+
* edge). Neither → unfiltered.
|
|
25528
|
+
*
|
|
25529
|
+
* ```typescript
|
|
25530
|
+
* const el = document.createElement('jssm-on');
|
|
25531
|
+
* el.setAttribute('event', 'entry');
|
|
25532
|
+
* el.setAttribute('state', 'paid');
|
|
25533
|
+
* el.setAttribute('handler', 'onPaid');
|
|
25534
|
+
* parse_jssm_on_element(el);
|
|
25535
|
+
* // => { event: 'entry', handler_name: 'onPaid', inline_body: undefined,
|
|
25536
|
+
* // once: false, name: undefined, filter: { state: 'paid' } }
|
|
25537
|
+
* ```
|
|
25538
|
+
*
|
|
25539
|
+
* @param el - The `<jssm-on>` element to parse.
|
|
25540
|
+
* @returns A validated {@link ParsedJssmOn} record.
|
|
25541
|
+
* @throws If `event` is missing, unknown, both handler forms are
|
|
25542
|
+
* supplied, or neither handler form is supplied.
|
|
25543
|
+
*/
|
|
25544
|
+
function parse_jssm_on_element(el) {
|
|
25545
|
+
const event_attr = el.getAttribute('event');
|
|
25546
|
+
if (event_attr === null || event_attr.trim().length === 0) {
|
|
25547
|
+
throw new Error('<jssm-on>: missing required `event` attribute');
|
|
25548
|
+
}
|
|
25549
|
+
const event = event_attr.trim();
|
|
25550
|
+
if (!JSSM_ON_EVENT_NAMES.has(event)) {
|
|
25551
|
+
throw new Error(`<jssm-on>: unknown event "${event}"`);
|
|
25552
|
+
}
|
|
25553
|
+
const handler_attr = el.getAttribute('handler');
|
|
25554
|
+
const handler_name = (handler_attr !== null && handler_attr.trim().length > 0)
|
|
25555
|
+
? handler_attr.trim()
|
|
25556
|
+
: undefined;
|
|
25557
|
+
// textContent on a connected HTMLElement is always a string, so the
|
|
25558
|
+
// historical `?? ''` fallback never executed. Use a direct cast here
|
|
25559
|
+
// and let test cases that supply a literal `null` (defensive coverage)
|
|
25560
|
+
// hit the `=== null` branch instead — that branch is reachable via
|
|
25561
|
+
// Object.defineProperty in tests, where the `??` form would be a dead
|
|
25562
|
+
// operator.
|
|
25563
|
+
const body_text = el.textContent;
|
|
25564
|
+
const inline_body = (body_text !== null && body_text.trim().length > 0) ? body_text : undefined;
|
|
25565
|
+
if (handler_name !== undefined && inline_body !== undefined) {
|
|
25566
|
+
throw new Error('<jssm-on>: specify handler="name" OR inline body, not both');
|
|
25567
|
+
}
|
|
25568
|
+
if (handler_name === undefined && inline_body === undefined) {
|
|
25569
|
+
throw new Error('<jssm-on>: must specify handler="name" or an inline body');
|
|
25570
|
+
}
|
|
25571
|
+
const once_attr = el.hasAttribute('once');
|
|
25572
|
+
const name_attr = el.getAttribute('name');
|
|
25573
|
+
const name = (name_attr !== null && name_attr.trim().length > 0) ? name_attr.trim() : undefined;
|
|
25574
|
+
// Build the filter, but only honour attributes that apply to this event.
|
|
25575
|
+
// Unknown filter attributes for an event are silently ignored, matching
|
|
25576
|
+
// the documented semantics in the issue.
|
|
25577
|
+
let filter;
|
|
25578
|
+
if (event === 'entry' || event === 'exit') {
|
|
25579
|
+
const state_attr = el.getAttribute('state');
|
|
25580
|
+
if (state_attr !== null && state_attr.length > 0) {
|
|
25581
|
+
filter = { state: state_attr };
|
|
25582
|
+
}
|
|
25583
|
+
}
|
|
25584
|
+
else if (event === 'transition') {
|
|
25585
|
+
const from_attr = el.getAttribute('from');
|
|
25586
|
+
const to_attr = el.getAttribute('to');
|
|
25587
|
+
const candidate = {};
|
|
25588
|
+
if (from_attr !== null && from_attr.length > 0) {
|
|
25589
|
+
candidate.from = from_attr;
|
|
25590
|
+
}
|
|
25591
|
+
if (to_attr !== null && to_attr.length > 0) {
|
|
25592
|
+
candidate.to = to_attr;
|
|
25593
|
+
}
|
|
25594
|
+
if (Object.keys(candidate).length > 0) {
|
|
25595
|
+
filter = candidate;
|
|
25596
|
+
}
|
|
25597
|
+
}
|
|
25598
|
+
return {
|
|
25599
|
+
event,
|
|
25600
|
+
handler_name,
|
|
25601
|
+
inline_body,
|
|
25602
|
+
once: once_attr,
|
|
25603
|
+
name,
|
|
25604
|
+
filter
|
|
25605
|
+
};
|
|
25606
|
+
}
|
|
25607
|
+
/**
|
|
25608
|
+
* Optional global registry that `<jssm-on>` (and, later, `<jssm-hook>`)
|
|
25609
|
+
* consult first when resolving a `handler="name"` attribute. Consumers
|
|
25610
|
+
* register named handlers here in a strict-CSP environment where a stray
|
|
25611
|
+
* `globalThis[name]` isn't acceptable. Falls through to `globalThis[name]`
|
|
25612
|
+
* if the registry has no entry.
|
|
25613
|
+
*
|
|
25614
|
+
* Intentionally a `Map<string, Function>` rather than a class with methods,
|
|
25615
|
+
* so consumers can use any of `.get`, `.set`, `.delete`, `.clear` directly
|
|
25616
|
+
* without a thin wrapper API.
|
|
25617
|
+
*/
|
|
25618
|
+
const jssm_handler_registry = new Map();
|
|
25619
|
+
/**
|
|
25620
|
+
* Resolve a named handler from the registry, then from `globalThis`.
|
|
25621
|
+
* Throws if neither lookup finds a function — earlier failure here is
|
|
25622
|
+
* better than a delayed "is not a function" at first event delivery.
|
|
25623
|
+
*
|
|
25624
|
+
* @param name - The handler name as supplied by `handler="..."`.
|
|
25625
|
+
* @returns The resolved function.
|
|
25626
|
+
* @throws If no function is registered under `name`.
|
|
25627
|
+
*/
|
|
25628
|
+
function resolve_named_handler(name) {
|
|
25629
|
+
const from_registry = jssm_handler_registry.get(name);
|
|
25630
|
+
if (typeof from_registry === 'function') {
|
|
25631
|
+
return from_registry;
|
|
25632
|
+
}
|
|
25633
|
+
const from_global = globalThis[name];
|
|
25634
|
+
if (typeof from_global === 'function') {
|
|
25635
|
+
return from_global;
|
|
25636
|
+
}
|
|
25637
|
+
throw new Error(`<jssm-on>: handler "${name}" not found in registry or globalThis`);
|
|
25638
|
+
}
|
|
25639
|
+
/**
|
|
25640
|
+
* Compile an inline-body string into a handler function whose single
|
|
25641
|
+
* parameter is `e` (the event detail object). Uses the same dynamic
|
|
25642
|
+
* `Function(...)` constructor that browsers use internally for inline
|
|
25643
|
+
* event-handler attributes such as `<a onclick="...">`; the input here
|
|
25644
|
+
* is consumer-authored markup, never network data, so the surface is
|
|
25645
|
+
* exactly that of an inline event-handler attribute and the same CSP
|
|
25646
|
+
* caveats apply (strict CSP without `'unsafe-eval'` blocks it). A
|
|
25647
|
+
* `//# sourceURL=jssm-on:N` pragma is appended so devtools stack traces
|
|
25648
|
+
* point at a meaningful name.
|
|
25649
|
+
*
|
|
25650
|
+
* @param body - The inline JS body (function body, not full function).
|
|
25651
|
+
* @param source_id - A short identifier for the sourceURL pragma.
|
|
25652
|
+
* @returns The compiled handler.
|
|
25653
|
+
*/
|
|
25654
|
+
function compile_inline_body(body, source_id) {
|
|
25655
|
+
const wrapped = `${body}\n//# sourceURL=jssm-on:${source_id}`;
|
|
25656
|
+
// The Function constructor is intentional here — see the docblock above
|
|
25657
|
+
// for the rationale and the CSP caveat. Equivalent to how browsers wire
|
|
25658
|
+
// up inline event handlers; the input is consumer-authored markup.
|
|
25659
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
25660
|
+
return new Function('e', wrapped); // skipcq: JS-0086
|
|
25661
|
+
}
|
|
25275
25662
|
/**
|
|
25276
25663
|
* Resolve a `<jssm-instance>`'s FSL source from the three legal channels:
|
|
25277
25664
|
* the `fsl=""` attribute, a child `<script type="text/fsl">`, and the
|
|
@@ -25387,44 +25774,33 @@ class JssmInstance extends i {
|
|
|
25387
25774
|
* connection.
|
|
25388
25775
|
*/
|
|
25389
25776
|
this._machine = undefined;
|
|
25777
|
+
/**
|
|
25778
|
+
* Live unsubscribe callbacks for #645 `<jssm-bind>` / `data-jssm-bind`
|
|
25779
|
+
* projections. Every entry must be invoked exactly once during
|
|
25780
|
+
* {@link disconnectedCallback}.
|
|
25781
|
+
*/
|
|
25782
|
+
this._unsubs = [];
|
|
25783
|
+
/**
|
|
25784
|
+
* Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`
|
|
25785
|
+
* subscription installed from a `<jssm-on>` child during
|
|
25786
|
+
* `connectedCallback`. Walked in `disconnectedCallback`.
|
|
25787
|
+
*/
|
|
25788
|
+
this._on_unsubscribes = [];
|
|
25390
25789
|
/**
|
|
25391
25790
|
* Per-instance registry of named hook handlers consulted before
|
|
25392
25791
|
* `globalThis` when resolving `<jssm-hook handler="name">`.
|
|
25393
|
-
*
|
|
25394
|
-
* Initialized to an empty `Map`; consumers may populate it before the
|
|
25395
|
-
* element connects to provide handlers without polluting global scope —
|
|
25396
|
-
* useful for module-scoped SPAs where strict CSP blocks inline-body hooks.
|
|
25397
|
-
*
|
|
25398
|
-
* @see {@link parse_hook_element}
|
|
25399
25792
|
*/
|
|
25400
25793
|
this.registry = new Map();
|
|
25401
25794
|
/**
|
|
25402
|
-
* Descriptors for hooks this WC installed at connect time
|
|
25403
|
-
* `disconnectedCallback` to call `remove_hook` for each so the underlying
|
|
25404
|
-
* machine doesn't leak handlers when the element is detached.
|
|
25405
|
-
*
|
|
25406
|
-
* Captured at install time because `remove_hook` matches by descriptor
|
|
25407
|
-
* shape (not handler identity), and we need to record the wrapped handler
|
|
25408
|
-
* we passed to `set_hook` to undo the registration cleanly. Stored as
|
|
25409
|
-
* `unknown[]` and cast at the call site because jssm's `HookDescription`
|
|
25410
|
-
* is a discriminated union whose discriminator is only known at runtime.
|
|
25795
|
+
* Descriptors for hooks this WC installed at connect time.
|
|
25411
25796
|
*/
|
|
25412
25797
|
this._installed_hooks = [];
|
|
25413
25798
|
/**
|
|
25414
|
-
* Counter
|
|
25415
|
-
* for its `//# sourceURL=jssm-hook:N` annotation. Per-instance so that
|
|
25416
|
-
* multiple `<jssm-instance>` elements on a page don't share numbering.
|
|
25799
|
+
* Counter for compiled inline-body hook debug ids.
|
|
25417
25800
|
*/
|
|
25418
25801
|
this._hook_debug_counter = 0;
|
|
25419
25802
|
/**
|
|
25420
|
-
*
|
|
25421
|
-
* discovery so {@link disconnectedCallback} can remove each one with the
|
|
25422
|
-
* same handler reference originally passed to `addEventListener`.
|
|
25423
|
-
*
|
|
25424
|
-
* Listeners installed via the dedicated `<jssm-action>` tag form may target
|
|
25425
|
-
* elements outside the host (its `selector` is resolved against the host,
|
|
25426
|
-
* but matching elements live in the document tree), so cleanup must be
|
|
25427
|
-
* explicit — relying on the host's GC is not sufficient.
|
|
25803
|
+
* DOM listeners installed by `<jssm-action>` / `data-jssm-action` discovery.
|
|
25428
25804
|
*/
|
|
25429
25805
|
this._action_listeners = [];
|
|
25430
25806
|
}
|
|
@@ -25493,9 +25869,54 @@ class JssmInstance extends i {
|
|
|
25493
25869
|
// and dispatch DOM CustomEvents from this element.
|
|
25494
25870
|
// #641: <jssm-hook> declarative discovery.
|
|
25495
25871
|
this._install_declarative_hooks();
|
|
25496
|
-
//
|
|
25872
|
+
// #643: <jssm-on> declarative event observation.
|
|
25873
|
+
this._install_jssm_on_children();
|
|
25874
|
+
// #645: discover <jssm-bind> tags and `data-jssm-bind` descendants,
|
|
25875
|
+
// install live machine-to-DOM projections.
|
|
25876
|
+
this._unsubs.push(...install_bindings(this, this._machine));
|
|
25877
|
+
// #640: <jssm-action> DOM event → machine action wiring.
|
|
25497
25878
|
this._discover_jssm_actions();
|
|
25498
|
-
|
|
25879
|
+
}
|
|
25880
|
+
/**
|
|
25881
|
+
* Discover direct-child `<jssm-on>` elements and install their
|
|
25882
|
+
* subscriptions on the owned machine. Per #643:
|
|
25883
|
+
*
|
|
25884
|
+
* - Direct children only (`:scope > jssm-on`). Deeper nesting is the
|
|
25885
|
+
* responsibility of a future MutationObserver-driven v2.
|
|
25886
|
+
* - Each `<jssm-on>` is parsed by {@link parse_jssm_on_element}, which
|
|
25887
|
+
* enforces the form / event-name / filter rules.
|
|
25888
|
+
* - Handlers come from {@link resolve_named_handler} (form A) or
|
|
25889
|
+
* {@link compile_inline_body} (form B), and the result is installed
|
|
25890
|
+
* via `machine.on(...)` or `machine.once(...)` depending on the
|
|
25891
|
+
* element's `once` attribute.
|
|
25892
|
+
* - Every returned unsubscribe is tracked in {@link _on_unsubscribes}
|
|
25893
|
+
* so {@link disconnectedCallback} can release them all.
|
|
25894
|
+
*
|
|
25895
|
+
* Called once from `connectedCallback` after the machine has been
|
|
25896
|
+
* constructed. Any error thrown by parsing or resolution propagates
|
|
25897
|
+
* out so it surfaces via jsdom's error event (matching the rest of
|
|
25898
|
+
* `<jssm-instance>`'s "fail loud at connect" policy).
|
|
25899
|
+
*/
|
|
25900
|
+
_install_jssm_on_children() {
|
|
25901
|
+
const machine = this._machine;
|
|
25902
|
+
const on_nodes = this.querySelectorAll(':scope > jssm-on');
|
|
25903
|
+
let index = 0;
|
|
25904
|
+
for (const el of Array.from(on_nodes)) {
|
|
25905
|
+
index += 1;
|
|
25906
|
+
const parsed = parse_jssm_on_element(el);
|
|
25907
|
+
const handler = parsed.handler_name !== undefined
|
|
25908
|
+
? resolve_named_handler(parsed.handler_name)
|
|
25909
|
+
: compile_inline_body(parsed.inline_body, String(index));
|
|
25910
|
+
// Argument shape: machine.on(name, handler) when no filter, or
|
|
25911
|
+
// machine.on(name, filter, handler) when filtered. Same for once.
|
|
25912
|
+
// `as any` collapses the per-event detail typing — the WC is a
|
|
25913
|
+
// schema-erased entry point and the type-safety belongs upstream.
|
|
25914
|
+
const subscribe = parsed.once ? machine.once.bind(machine) : machine.on.bind(machine);
|
|
25915
|
+
const unsubscribe = parsed.filter === undefined
|
|
25916
|
+
? subscribe(parsed.event, handler)
|
|
25917
|
+
: subscribe(parsed.event, parsed.filter, handler);
|
|
25918
|
+
this._on_unsubscribes.push(unsubscribe);
|
|
25919
|
+
}
|
|
25499
25920
|
}
|
|
25500
25921
|
/**
|
|
25501
25922
|
* Discover every direct-child `<jssm-hook>` element and install each
|
|
@@ -25503,17 +25924,6 @@ class JssmInstance extends i {
|
|
|
25503
25924
|
* adapter that lets user code write `m.data = ...` and return `false` to
|
|
25504
25925
|
* cancel — see {@link make_hook_proxy} and the issue (#641) doc-comment
|
|
25505
25926
|
* for the full contract.
|
|
25506
|
-
*
|
|
25507
|
-
* Direct children only (the `:scope > jssm-hook` selector) so that nested
|
|
25508
|
-
* `<jssm-instance>` elements don't have their child hooks installed on
|
|
25509
|
-
* the outer machine.
|
|
25510
|
-
*
|
|
25511
|
-
* Tracks every installed descriptor in `_installed_hooks` so that
|
|
25512
|
-
* `disconnectedCallback` can remove them on detach.
|
|
25513
|
-
*
|
|
25514
|
-
* @throws Error - On a malformed `<jssm-hook>` (mutual-exclusion violation,
|
|
25515
|
-
* unknown kind, unresolved name, or jssm's own missing-key
|
|
25516
|
-
* errors from `set_hook`).
|
|
25517
25927
|
*/
|
|
25518
25928
|
_install_declarative_hooks() {
|
|
25519
25929
|
const machine = this._machine;
|
|
@@ -25523,35 +25933,26 @@ class JssmInstance extends i {
|
|
|
25523
25933
|
const spec = parse_hook_element(el, debug_id, this.registry);
|
|
25524
25934
|
const wrapped = wrap_user_handler(spec, machine);
|
|
25525
25935
|
const desc = build_hook_descriptor(spec, wrapped);
|
|
25526
|
-
// `desc` is shaped from runtime kind discrimination; jssm's typed
|
|
25527
|
-
// `HookDescription` is a static discriminated union that TS can't
|
|
25528
|
-
// unify with our runtime-built object, hence the cast.
|
|
25529
25936
|
machine.set_hook(desc);
|
|
25530
25937
|
this._installed_hooks.push(desc);
|
|
25531
25938
|
}
|
|
25532
25939
|
}
|
|
25533
25940
|
/**
|
|
25534
25941
|
* Prefix used in synthetic `//# sourceURL=jssm-hook:<prefix><n>` annotations
|
|
25535
|
-
* for inline-body hooks compiled by this element.
|
|
25536
|
-
* `id` when present so multi-instance pages can tell sources apart in
|
|
25537
|
-
* devtools.
|
|
25942
|
+
* for inline-body hooks compiled by this element.
|
|
25538
25943
|
*/
|
|
25539
25944
|
_hook_id_prefix() {
|
|
25540
25945
|
const host_id = this.getAttribute('id');
|
|
25541
25946
|
return host_id !== null && host_id.length > 0 ? `${host_id}-` : '';
|
|
25542
25947
|
}
|
|
25543
25948
|
/**
|
|
25544
|
-
* Lifecycle hook.
|
|
25545
|
-
* `<jssm-hook
|
|
25546
|
-
*
|
|
25547
|
-
* machine itself is not destroyed (consumers can reuse it).
|
|
25548
|
-
*
|
|
25549
|
-
* Future tickets #638/#643/#645 will extend this to drop other
|
|
25550
|
-
* subscriptions / listeners installed by their respective tags.
|
|
25949
|
+
* Lifecycle hook. Cleans up everything the WC installed at connect: hook
|
|
25950
|
+
* registrations from `<jssm-hook>`, event subscriptions from `<jssm-on>`,
|
|
25951
|
+
* and DOM listeners from `<jssm-action>` / `data-jssm-action`.
|
|
25551
25952
|
*/
|
|
25552
25953
|
disconnectedCallback() {
|
|
25553
25954
|
super.disconnectedCallback();
|
|
25554
|
-
// TODO #638: unsubscribe from machine.on(...) handlers.
|
|
25955
|
+
// TODO #638: unsubscribe from any direct machine.on(...) handlers added by host.
|
|
25555
25956
|
// #641: remove installed hooks.
|
|
25556
25957
|
if (this._machine !== undefined) {
|
|
25557
25958
|
const machine = this._machine;
|
|
@@ -25560,10 +25961,20 @@ class JssmInstance extends i {
|
|
|
25560
25961
|
}
|
|
25561
25962
|
}
|
|
25562
25963
|
this._installed_hooks = [];
|
|
25563
|
-
//
|
|
25564
|
-
|
|
25565
|
-
|
|
25566
|
-
|
|
25964
|
+
// #643: release every subscription installed from a <jssm-on> child.
|
|
25965
|
+
for (const off of this._on_unsubscribes) {
|
|
25966
|
+
try {
|
|
25967
|
+
off();
|
|
25968
|
+
}
|
|
25969
|
+
catch ( /* swallow — cleanup must not throw past us */_a) { /* swallow — cleanup must not throw past us */ }
|
|
25970
|
+
}
|
|
25971
|
+
this._on_unsubscribes = [];
|
|
25972
|
+
// #645: tear down every live binding.
|
|
25973
|
+
for (const off of this._unsubs) {
|
|
25974
|
+
off();
|
|
25975
|
+
}
|
|
25976
|
+
this._unsubs = [];
|
|
25977
|
+
// #640: remove DOM listeners installed via <jssm-action> / data-jssm-action.
|
|
25567
25978
|
for (const entry of this._action_listeners) {
|
|
25568
25979
|
entry.target.removeEventListener(entry.event, entry.handler);
|
|
25569
25980
|
}
|
|
@@ -25571,30 +25982,12 @@ class JssmInstance extends i {
|
|
|
25571
25982
|
}
|
|
25572
25983
|
/**
|
|
25573
25984
|
* Wire DOM events to machine actions, using the two declarative forms from
|
|
25574
|
-
* issue #640
|
|
25575
|
-
*
|
|
25576
|
-
*
|
|
25577
|
-
* `data-jssm-action="<name>"` attribute receives a listener on the
|
|
25578
|
-
* event named by `data-jssm-event` (default `click`).
|
|
25579
|
-
* 2. Dedicated tag form: each direct `<jssm-action>` child of the host
|
|
25580
|
-
* supplies a CSS `selector` (scoped to the host), an `action`, and an
|
|
25581
|
-
* optional `event` (default `click`); every matching descendant
|
|
25582
|
-
* receives a listener configured by the tag's attributes.
|
|
25583
|
-
*
|
|
25584
|
-
* Both forms support optional `from-state` guards (dispatch only when the
|
|
25585
|
-
* machine's current state matches), `from-property` data extraction (pass
|
|
25586
|
-
* the source element's named property as the action's data argument), and
|
|
25587
|
-
* `prevent-default` / `stop-propagation` modifiers.
|
|
25588
|
-
*
|
|
25589
|
-
* Every installed listener is recorded in {@link _action_listeners} so
|
|
25590
|
-
* {@link disconnectedCallback} can detach them cleanly.
|
|
25985
|
+
* issue #640. Both forms support optional `from-state` guards,
|
|
25986
|
+
* `from-property` data extraction, and `prevent-default` /
|
|
25987
|
+
* `stop-propagation` modifiers.
|
|
25591
25988
|
*/
|
|
25592
25989
|
_discover_jssm_actions() {
|
|
25593
25990
|
var _a, _b, _c, _d;
|
|
25594
|
-
// Inline attribute form: `[data-jssm-action]` descendants. Per the
|
|
25595
|
-
// ticket, we scan the host's light DOM (not the shadow tree, which is
|
|
25596
|
-
// owned by us) and skip any element living inside a `<jssm-action>` tag
|
|
25597
|
-
// — those tags are pure data markup, never the source of an event.
|
|
25598
25991
|
const inline_targets = Array.from(this.querySelectorAll('[data-jssm-action]')).filter(el => el.closest('jssm-action') === null);
|
|
25599
25992
|
for (const el of inline_targets) {
|
|
25600
25993
|
this._install_action_listener({
|
|
@@ -25607,16 +26000,11 @@ class JssmInstance extends i {
|
|
|
25607
26000
|
stop_propagation: 'jssmStopPropagation' in el.dataset,
|
|
25608
26001
|
});
|
|
25609
26002
|
}
|
|
25610
|
-
// Dedicated tag form: direct `<jssm-action>` children of the host.
|
|
25611
|
-
// `:scope >` keeps a nested `<jssm-instance>`'s actions from being
|
|
25612
|
-
// claimed by an outer host.
|
|
25613
26003
|
const tags = this.querySelectorAll(':scope > jssm-action');
|
|
25614
26004
|
for (const tag of Array.from(tags)) {
|
|
25615
26005
|
const selector = tag.getAttribute('selector');
|
|
25616
26006
|
const action_name = tag.getAttribute('action');
|
|
25617
26007
|
if (selector === null || action_name === null) {
|
|
25618
|
-
// Required attrs missing — skip, but don't throw: a malformed tag
|
|
25619
|
-
// shouldn't break the rest of the host's wiring.
|
|
25620
26008
|
continue;
|
|
25621
26009
|
}
|
|
25622
26010
|
const event_name = (_b = tag.getAttribute('event')) !== null && _b !== void 0 ? _b : 'click';
|
|
@@ -25640,18 +26028,7 @@ class JssmInstance extends i {
|
|
|
25640
26028
|
}
|
|
25641
26029
|
/**
|
|
25642
26030
|
* Attach one DOM listener that translates a DOM event into a
|
|
25643
|
-
* `machine.action(...)` call, honoring the configured modifiers.
|
|
25644
|
-
* listener is recorded in {@link _action_listeners} so it can be removed
|
|
25645
|
-
* on disconnect.
|
|
25646
|
-
*
|
|
25647
|
-
* @param config - Listener configuration.
|
|
25648
|
-
* @param config.source - Element to attach the listener to.
|
|
25649
|
-
* @param config.event_name - DOM event to listen for.
|
|
25650
|
-
* @param config.action_name - Action to dispatch on the machine.
|
|
25651
|
-
* @param config.from_state - If set, only fire when `machine.state() === from_state`.
|
|
25652
|
-
* @param config.from_property - If set, pass `source[from_property]` as the action's data argument.
|
|
25653
|
-
* @param config.prevent_default - If true, call `e.preventDefault()` before checking the guard.
|
|
25654
|
-
* @param config.stop_propagation - If true, call `e.stopPropagation()` before checking the guard.
|
|
26031
|
+
* `machine.action(...)` call, honoring the configured modifiers.
|
|
25655
26032
|
*/
|
|
25656
26033
|
_install_action_listener(config) {
|
|
25657
26034
|
const handler = (e) => {
|
|
@@ -25661,7 +26038,6 @@ class JssmInstance extends i {
|
|
|
25661
26038
|
if (config.stop_propagation) {
|
|
25662
26039
|
e.stopPropagation();
|
|
25663
26040
|
}
|
|
25664
|
-
// Guard: skip dispatch when the machine isn't in the required state.
|
|
25665
26041
|
if (config.from_state !== undefined && this.state() !== config.from_state) {
|
|
25666
26042
|
return;
|
|
25667
26043
|
}
|