jssm 5.136.0 → 5.138.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.
@@ -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.136.0";
21330
+ const version = "5.138.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,208 @@ function abstract_everything_hook_step(maybe_hook, hook_args) {
25070
25070
  }
25071
25071
  }
25072
25072
 
25073
+ const VALID_KINDS = new Set([
25074
+ 'hook',
25075
+ 'named',
25076
+ 'any transition',
25077
+ 'standard transition',
25078
+ 'main transition',
25079
+ 'forced transition',
25080
+ 'entry',
25081
+ 'exit',
25082
+ 'any action',
25083
+ 'global action',
25084
+ ]);
25085
+ /**
25086
+ * Build a {@link JssmHookProxy} that wraps an arbitrary hook context object.
25087
+ *
25088
+ * The context shape varies by hook kind (`from`/`to`/`action` may be absent
25089
+ * for transition-kind hooks), so this normalizes the shape via optional
25090
+ * fields and exposes mutable `data` while keeping the rest read-only.
25091
+ *
25092
+ * The `machine` parameter is used only for `state()`, so unit tests can
25093
+ * substitute any object with a `state(): unknown` method.
25094
+ *
25095
+ * @param ctx - Raw hook context passed by jssm.
25096
+ * @param machine - The owning machine; used for the `state()` accessor.
25097
+ * @returns A proxy object suitable for passing to a user handler.
25098
+ */
25099
+ function make_hook_proxy(ctx, machine) {
25100
+ return {
25101
+ get data() {
25102
+ return ctx.data;
25103
+ },
25104
+ set data(next) {
25105
+ ctx.data = next;
25106
+ },
25107
+ get from() {
25108
+ return ctx.from;
25109
+ },
25110
+ get to() {
25111
+ return ctx.to;
25112
+ },
25113
+ get action() {
25114
+ return ctx.action;
25115
+ },
25116
+ state() {
25117
+ return String(machine.state());
25118
+ },
25119
+ };
25120
+ }
25121
+ /**
25122
+ * Compile a textContent body into a callable user handler.
25123
+ *
25124
+ * Uses dynamic function construction — the same primitive browsers use
25125
+ * internally for `<a onclick="...">` and `setTimeout(stringBody, ms)`.
25126
+ * Strict CSP without `'unsafe-eval'` blocks this and the call will throw;
25127
+ * consumers should fall back to the `handler="name"` form there.
25128
+ *
25129
+ * Prepends a `//# sourceURL=` comment so devtools surface a meaningful name
25130
+ * in stack traces instead of `anonymous`.
25131
+ *
25132
+ * @param body - Trimmed textContent of the `<jssm-hook>` element.
25133
+ * @param debug_id - Identifier appended to the synthetic sourceURL.
25134
+ * @returns The compiled handler.
25135
+ */
25136
+ function compile_inline_body(body, debug_id) {
25137
+ const annotated = `//# sourceURL=jssm-hook:${debug_id}\n${body}`;
25138
+ const ctor = Function;
25139
+ return new ctor('m', annotated);
25140
+ }
25141
+ /**
25142
+ * Resolve a `handler="name"` attribute to a callable by consulting first the
25143
+ * optional in-WC registry, then `globalThis[name]`. Throws a clear error if
25144
+ * neither resolves.
25145
+ *
25146
+ * @param name - The handler name from the `handler=""` attribute.
25147
+ * @param registry - Optional in-WC registry to consult first.
25148
+ * @returns The resolved handler.
25149
+ * @throws Error - If no callable of that name is found in either location.
25150
+ */
25151
+ function resolve_named_handler(name, registry) {
25152
+ if (registry !== undefined) {
25153
+ const registered = registry.get(name);
25154
+ if (registered !== undefined) {
25155
+ return registered;
25156
+ }
25157
+ }
25158
+ const global = globalThis[name];
25159
+ if (typeof global === 'function') {
25160
+ return global;
25161
+ }
25162
+ throw new Error(`<jssm-hook handler="${name}">: handler not found in registry or globalThis`);
25163
+ }
25164
+ /**
25165
+ * Validate and normalize a `<jssm-hook kind="...">` value, defaulting to
25166
+ * `"hook"` when the attribute is absent. Throws on unknown kinds rather
25167
+ * than silently doing nothing later.
25168
+ *
25169
+ * @param raw - The raw attribute value, or null if not present.
25170
+ * @returns The normalized {@link JssmHookKind}.
25171
+ * @throws Error - On an unknown kind.
25172
+ */
25173
+ function normalize_hook_kind(raw) {
25174
+ if (raw === null || raw === undefined || raw === '') {
25175
+ return 'hook';
25176
+ }
25177
+ if (!VALID_KINDS.has(raw)) {
25178
+ throw new Error(`<jssm-hook kind="${raw}">: unknown hook kind (expected one of: ${[...VALID_KINDS].join(', ')})`);
25179
+ }
25180
+ return raw;
25181
+ }
25182
+ /**
25183
+ * Parse a single `<jssm-hook>` element into a {@link JssmHookInstallSpec}.
25184
+ *
25185
+ * Validates the mutual-exclusion rule between `handler="name"` and inline
25186
+ * body, defaults `kind` to `"hook"`, resolves named handlers against the
25187
+ * optional registry then `globalThis`, and compiles inline bodies via
25188
+ * dynamic function construction. Conditional-required attributes (e.g.
25189
+ * `from`/`to` for `kind="hook"`) are NOT validated here — `set_hook` will
25190
+ * throw with its own clear errors on missing pieces, which keeps the
25191
+ * error surface single-sourced.
25192
+ *
25193
+ * @param el - The `<jssm-hook>` element to parse.
25194
+ * @param debug_id - Identifier used in the inline body's sourceURL.
25195
+ * @param registry - Optional in-WC registry of named handlers.
25196
+ * @returns A {@link JssmHookInstallSpec} describing what to install.
25197
+ * @throws Error - On mutual-exclusion violation, unknown kind, or unresolved name.
25198
+ */
25199
+ function parse_hook_element(el, debug_id, registry) {
25200
+ var _a, _b, _c, _d;
25201
+ const handler_attr = el.getAttribute('handler');
25202
+ const raw_text = el.textContent;
25203
+ const body_text = (raw_text === null ? '' : raw_text).trim();
25204
+ if (handler_attr !== null && body_text.length > 0) {
25205
+ throw new Error('<jssm-hook>: specify handler="name" OR inline body, not both');
25206
+ }
25207
+ if (handler_attr === null && body_text.length === 0) {
25208
+ throw new Error('<jssm-hook>: must specify either handler="name" attribute or an inline body');
25209
+ }
25210
+ const user_handler = handler_attr !== null
25211
+ ? resolve_named_handler(handler_attr, registry)
25212
+ : compile_inline_body(body_text, debug_id);
25213
+ const kind = normalize_hook_kind(el.getAttribute('kind'));
25214
+ // Convert null → undefined so downstream descriptors omit absent keys.
25215
+ const from = (_a = el.getAttribute('from')) !== null && _a !== void 0 ? _a : undefined;
25216
+ const to = (_b = el.getAttribute('to')) !== null && _b !== void 0 ? _b : undefined;
25217
+ const action = (_c = el.getAttribute('action')) !== null && _c !== void 0 ? _c : undefined;
25218
+ const name = (_d = el.getAttribute('name')) !== null && _d !== void 0 ? _d : undefined;
25219
+ return { kind, name, from, to, action, user_handler };
25220
+ }
25221
+ /**
25222
+ * Wrap a {@link JssmHookUserHandler} so that jssm's native hook contract is
25223
+ * satisfied: the user gets a friendly proxy, the proxy's mutated `data`
25224
+ * becomes the `HookComplexResult.data`, and an explicit `false` return
25225
+ * cancels the transition.
25226
+ *
25227
+ * Any non-`false` return — including `undefined`, `true`, or an arbitrary
25228
+ * object — allows the transition. This matches the contract spelled out
25229
+ * in the issue (#641): "return false cancels; anything else allows".
25230
+ *
25231
+ * @param spec - The parsed install spec carrying the user handler.
25232
+ * @param machine - The owning machine; used by the proxy's `state()`.
25233
+ * @returns A wrapped handler suitable for `set_hook`.
25234
+ */
25235
+ function wrap_user_handler(spec, machine) {
25236
+ const user = spec.user_handler;
25237
+ return (ctx) => {
25238
+ const proxy = make_hook_proxy(ctx, machine);
25239
+ const result = user(proxy);
25240
+ if (result === false) {
25241
+ return false;
25242
+ }
25243
+ return { pass: true, data: proxy.data };
25244
+ };
25245
+ }
25246
+ /**
25247
+ * Build the typed descriptor object passed to `machine.set_hook` (and later
25248
+ * to `machine.remove_hook` for cleanup) from a parsed {@link JssmHookInstallSpec}
25249
+ * and the wrapped handler.
25250
+ *
25251
+ * For kinds that need `from`/`to`/`action`, the descriptor includes those.
25252
+ * Missing required keys produce `undefined` here; jssm's `set_hook` will
25253
+ * surface the error with its own clear message so we don't duplicate
25254
+ * validation.
25255
+ *
25256
+ * Return type is `unknown` because jssm's `HookDescription` is a
25257
+ * discriminated union and our runtime-discriminator value can't be tracked
25258
+ * by TypeScript across the build. The WC casts at the `set_hook` call site.
25259
+ *
25260
+ * @param spec - The parsed install spec.
25261
+ * @param wrapped - The wrapped (friendly-proxy) handler from {@link wrap_user_handler}.
25262
+ * @returns A descriptor object for `set_hook`/`remove_hook`.
25263
+ */
25264
+ function build_hook_descriptor(spec, wrapped) {
25265
+ const base = { kind: spec.kind, handler: wrapped };
25266
+ if (spec.from !== undefined)
25267
+ base.from = spec.from;
25268
+ if (spec.to !== undefined)
25269
+ base.to = spec.to;
25270
+ if (spec.action !== undefined)
25271
+ base.action = spec.action;
25272
+ return base;
25273
+ }
25274
+
25073
25275
  /**
25074
25276
  * Resolve a `<jssm-instance>`'s FSL source from the three legal channels:
25075
25277
  * the `fsl=""` attribute, a child `<script type="text/fsl">`, and the
@@ -25185,6 +25387,46 @@ class JssmInstance extends i {
25185
25387
  * connection.
25186
25388
  */
25187
25389
  this._machine = undefined;
25390
+ /**
25391
+ * Per-instance registry of named hook handlers consulted before
25392
+ * `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
+ */
25400
+ this.registry = new Map();
25401
+ /**
25402
+ * Descriptors for hooks this WC installed at connect time, used in
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.
25411
+ */
25412
+ this._installed_hooks = [];
25413
+ /**
25414
+ * Counter used to give each compiled inline-body hook a unique debug id
25415
+ * for its `//# sourceURL=jssm-hook:N` annotation. Per-instance so that
25416
+ * multiple `<jssm-instance>` elements on a page don't share numbering.
25417
+ */
25418
+ this._hook_debug_counter = 0;
25419
+ /**
25420
+ * Records every DOM listener installed by `<jssm-action>` / `data-jssm-action`
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.
25428
+ */
25429
+ this._action_listeners = [];
25188
25430
  }
25189
25431
  /**
25190
25432
  * Raw machine accessor. Returns the owned {@link Machine} instance.
@@ -25249,22 +25491,191 @@ class JssmInstance extends i {
25249
25491
  this.requestUpdate();
25250
25492
  // TODO #638: subscribe to machine.on('transition', ...) once available
25251
25493
  // and dispatch DOM CustomEvents from this element.
25252
- // TODO #641: <jssm-hook> discovery happens here.
25494
+ // #641: <jssm-hook> declarative discovery.
25495
+ this._install_declarative_hooks();
25253
25496
  // TODO #643: <jssm-on> discovery happens here.
25254
- // TODO #640: <jssm-action> discovery happens here.
25497
+ this._discover_jssm_actions();
25255
25498
  // TODO #645: <jssm-bind> discovery happens here.
25256
25499
  }
25257
25500
  /**
25258
- * Lifecycle hook. Cleans up any installed subscriptions. Currently a
25259
- * no-op (no subscriptions are installed in the base scaffolding) — the
25260
- * empty body is intentional so the future tickets #638/#641/#643/#645
25261
- * have a single canonical hook to extend.
25501
+ * Discover every direct-child `<jssm-hook>` element and install each
25502
+ * against the owned machine. Handlers are wrapped with the friendly-proxy
25503
+ * adapter that lets user code write `m.data = ...` and return `false` to
25504
+ * cancel see {@link make_hook_proxy} and the issue (#641) doc-comment
25505
+ * 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
+ */
25518
+ _install_declarative_hooks() {
25519
+ const machine = this._machine;
25520
+ const hook_els = this.querySelectorAll(':scope > jssm-hook');
25521
+ for (const el of Array.from(hook_els)) {
25522
+ const debug_id = `${this._hook_id_prefix()}${++this._hook_debug_counter}`;
25523
+ const spec = parse_hook_element(el, debug_id, this.registry);
25524
+ const wrapped = wrap_user_handler(spec, machine);
25525
+ 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
+ machine.set_hook(desc);
25530
+ this._installed_hooks.push(desc);
25531
+ }
25532
+ }
25533
+ /**
25534
+ * Prefix used in synthetic `//# sourceURL=jssm-hook:<prefix><n>` annotations
25535
+ * for inline-body hooks compiled by this element. Includes the element's
25536
+ * `id` when present so multi-instance pages can tell sources apart in
25537
+ * devtools.
25538
+ */
25539
+ _hook_id_prefix() {
25540
+ const host_id = this.getAttribute('id');
25541
+ return host_id !== null && host_id.length > 0 ? `${host_id}-` : '';
25542
+ }
25543
+ /**
25544
+ * Lifecycle hook. Removes every hook this WC installed via
25545
+ * `<jssm-hook>` discovery so the underlying machine doesn't leak handlers
25546
+ * when the element detaches. Called automatically by the browser; the
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.
25262
25551
  */
25263
25552
  disconnectedCallback() {
25264
25553
  super.disconnectedCallback();
25265
25554
  // TODO #638: unsubscribe from machine.on(...) handlers.
25266
- // TODO #641: remove installed hooks.
25555
+ // #641: remove installed hooks.
25556
+ if (this._machine !== undefined) {
25557
+ const machine = this._machine;
25558
+ for (const desc of this._installed_hooks) {
25559
+ machine.remove_hook(desc);
25560
+ }
25561
+ }
25562
+ this._installed_hooks = [];
25267
25563
  // TODO #643/#645: remove installed listeners / bindings.
25564
+ // Remove every listener installed during `<jssm-action>` / `data-jssm-action`
25565
+ // discovery. Using the original handler reference ensures `removeEventListener`
25566
+ // actually unbinds — anonymous re-creation here would silently leak.
25567
+ for (const entry of this._action_listeners) {
25568
+ entry.target.removeEventListener(entry.event, entry.handler);
25569
+ }
25570
+ this._action_listeners = [];
25571
+ }
25572
+ /**
25573
+ * Wire DOM events to machine actions, using the two declarative forms from
25574
+ * issue #640:
25575
+ *
25576
+ * 1. Inline attribute form: every descendant of the host carrying a
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.
25591
+ */
25592
+ _discover_jssm_actions() {
25593
+ 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
+ const inline_targets = Array.from(this.querySelectorAll('[data-jssm-action]')).filter(el => el.closest('jssm-action') === null);
25599
+ for (const el of inline_targets) {
25600
+ this._install_action_listener({
25601
+ source: el,
25602
+ event_name: (_a = el.dataset['jssmEvent']) !== null && _a !== void 0 ? _a : 'click',
25603
+ action_name: el.dataset['jssmAction'],
25604
+ from_state: el.dataset['jssmFromState'],
25605
+ from_property: el.dataset['jssmFromProperty'],
25606
+ prevent_default: 'jssmPreventDefault' in el.dataset,
25607
+ stop_propagation: 'jssmStopPropagation' in el.dataset,
25608
+ });
25609
+ }
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
+ const tags = this.querySelectorAll(':scope > jssm-action');
25614
+ for (const tag of Array.from(tags)) {
25615
+ const selector = tag.getAttribute('selector');
25616
+ const action_name = tag.getAttribute('action');
25617
+ 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
+ continue;
25621
+ }
25622
+ const event_name = (_b = tag.getAttribute('event')) !== null && _b !== void 0 ? _b : 'click';
25623
+ const from_state = (_c = tag.getAttribute('from-state')) !== null && _c !== void 0 ? _c : undefined;
25624
+ const from_property = (_d = tag.getAttribute('from-property')) !== null && _d !== void 0 ? _d : undefined;
25625
+ const prevent_default = tag.hasAttribute('prevent-default');
25626
+ const stop_propagation = tag.hasAttribute('stop-propagation');
25627
+ const sources = this.querySelectorAll(selector);
25628
+ for (const src of Array.from(sources)) {
25629
+ this._install_action_listener({
25630
+ source: src,
25631
+ event_name,
25632
+ action_name,
25633
+ from_state,
25634
+ from_property,
25635
+ prevent_default,
25636
+ stop_propagation,
25637
+ });
25638
+ }
25639
+ }
25640
+ }
25641
+ /**
25642
+ * Attach one DOM listener that translates a DOM event into a
25643
+ * `machine.action(...)` call, honoring the configured modifiers. The
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.
25655
+ */
25656
+ _install_action_listener(config) {
25657
+ const handler = (e) => {
25658
+ if (config.prevent_default) {
25659
+ e.preventDefault();
25660
+ }
25661
+ if (config.stop_propagation) {
25662
+ e.stopPropagation();
25663
+ }
25664
+ // Guard: skip dispatch when the machine isn't in the required state.
25665
+ if (config.from_state !== undefined && this.state() !== config.from_state) {
25666
+ return;
25667
+ }
25668
+ const data = config.from_property !== undefined
25669
+ ? config.source[config.from_property]
25670
+ : undefined;
25671
+ this.do(config.action_name, data);
25672
+ };
25673
+ config.source.addEventListener(config.event_name, handler);
25674
+ this._action_listeners.push({
25675
+ target: config.source,
25676
+ event: config.event_name,
25677
+ handler,
25678
+ });
25268
25679
  }
25269
25680
  /**
25270
25681
  * Reflect machine state onto host attributes and CSS custom properties.
package/dist/cdn/viz.js CHANGED
@@ -21352,7 +21352,7 @@ var constants = /*#__PURE__*/Object.freeze({
21352
21352
  * Useful for runtime diagnostics and for embedding in serialized machine
21353
21353
  * snapshots so that deserializers can detect version-skew.
21354
21354
  */
21355
- const version = "5.136.0";
21355
+ const version = "5.138.0";
21356
21356
 
21357
21357
  // whargarbl lots of these return arrays could/should be sets
21358
21358
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;