@vmz/core 0.3.13 → 0.3.14

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.
@@ -1,13 +1,22 @@
1
1
  import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normalize.js';
2
+ import { HTML_HASH_ATTR, htmlDigest } from '../../shared/html-digest.js';
2
3
  import { applyDirectHostBox } from '../direct-host-box.js';
3
4
  import { createUnknownComponentElement } from '../unknown-component.js';
4
5
  import { applyDomAttr, eventPropHandlerName } from './attributes.js';
5
- import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
6
+ import { createInstance, destroy, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
6
7
  import { isEventEntryStrategy, noteDomCreate, noteDomRemove, scheduleClientOn } from './lifecycle.js';
7
8
  import { components, resolveComponent } from './registry.js';
8
9
  import { beginEventFlush, endEventFlush, inferHandlerMethod, methodAllowsSyncEventFlush, registerBind, runDomEventHandler, runPatch, scheduleRefresh, unregisterBind, } from '../reactivity/patch-scheduler.js';
9
10
  let directPropBindSeq = 0;
10
11
  const SVG_NS = 'http://www.w3.org/2000/svg';
12
+ /** Vue accepts a function or an array of functions for one handler slot. */
13
+ function normalizeHandlerList(value) {
14
+ if (typeof value === 'function')
15
+ return [value];
16
+ if (Array.isArray(value))
17
+ return value.filter((fn) => typeof fn === 'function');
18
+ return [];
19
+ }
11
20
  /**
12
21
  * Nesting depth of inline `<svg>` subtrees during create/resume.
13
22
  *
@@ -30,19 +39,136 @@ function isSvgNode(node) {
30
39
  const tag = node.tagName;
31
40
  return tag != null && String(tag).toLowerCase() === 'svg' && ns == null;
32
41
  }
42
+ /** Component display name for a ctor or registry key (`<component :is>` too). */
43
+ function componentDisplayName(nameOrCtor) {
44
+ if (typeof nameOrCtor === 'function') {
45
+ const fn = nameOrCtor;
46
+ return fn.__vmzTag || fn.name || 'Component';
47
+ }
48
+ return nameOrCtor == null ? 'Component' : String(nameOrCtor);
49
+ }
50
+ /**
51
+ * Bind props into `host`, resolve the target component, create its child
52
+ * instance and run its create. Shared by the static `component()` and the
53
+ * `dynamicComponent()` remount path so both produce the same boundary.
54
+ */
55
+ function mountDirectComponentHost(host, hostInst, name, nameOrCtor, props, client) {
56
+ const rawProps = (props || {});
57
+ // `id` / `data-*` / `aria-*` are fallthrough attributes, not props: they
58
+ // belong on the host element (the component boundary in VMZ).
59
+ const hostAttrs = rawProps.__hostAttrs;
60
+ if (hostAttrs) {
61
+ for (const [k, v] of Object.entries(hostAttrs))
62
+ applyDomAttr(host, k, v);
63
+ }
64
+ const resolved = {};
65
+ for (const [k, v] of Object.entries(rawProps)) {
66
+ if (k === '__hostAttrs')
67
+ continue;
68
+ // Function props that already look like `onXxx` stay as handlers.
69
+ // Component `@event` never arrives here (emit uses onComponentEvent).
70
+ const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
71
+ if (onKey)
72
+ resolved[onKey] = v;
73
+ else if (typeof v === 'function')
74
+ resolved[k] = v.call(hostInst);
75
+ else
76
+ resolved[k] = v;
77
+ }
78
+ if (client) {
79
+ host.setAttribute('data-vmz-island', name);
80
+ host.setAttribute('data-vmz-client', String(client));
81
+ host.setAttribute('data-vmz-props', JSON.stringify(stripFns(resolved)));
82
+ if (isEventEntryStrategy(String(client))) {
83
+ host.setAttribute('data-vmz-entry', 'event');
84
+ }
85
+ // resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
86
+ scheduleClientOn(host, String(client), async () => {
87
+ const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : await resolveComponent(name);
88
+ if (!Ctor) {
89
+ // Replace placeholder island with leaf error node (do not throw page).
90
+ const err = createUnknownComponentElement(name, 'island');
91
+ host.replaceWith(err);
92
+ return;
93
+ }
94
+ const { resume } = await import('../resume/resume.js');
95
+ await resume(Ctor, host, { props: resolved, state: {} });
96
+ });
97
+ return host;
98
+ }
99
+ // Static Ctor import path: use Function directly (no registry lookup).
100
+ const Ctor = (typeof nameOrCtor === 'function' ? nameOrCtor : components[name]);
101
+ if (!Ctor) {
102
+ return createUnknownComponentElement(name, 'client');
103
+ }
104
+ // Inline chips use `display: contents` (`ui-direct-host-box`). Block surfaces
105
+ // keep a real box (DataTable select timed out when defaulting everything).
106
+ // A dynamic switch must reset the previous box before applying the new one.
107
+ // Writing an empty declaration at all still materialized a `style` attribute
108
+ // on hosts that never had one, so hydrate rewrote the SSR DOM for block
109
+ // surfaces. Only inline chips (`display: contents`) have a box to reset.
110
+ if (host.style && host.style.display === 'contents')
111
+ host.style.removeProperty('display');
112
+ applyDirectHostBox(host, name, Ctor);
113
+ const child = createInstance(Ctor, resolved);
114
+ if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
115
+ throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
116
+ }
117
+ // Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
118
+ // Enter the host's private child pool so nested create cannot see uncle siblings.
119
+ const adopt = directApi._resumeAdopt;
120
+ const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
121
+ // A child nested inside the parent's `v-for` must not feed the parent item
122
+ // patch sink: those patches belong to the child instance (`this`) and the
123
+ // parent's each replays them with the parent host, wiping child output.
124
+ const prevItemPatches = directApi._itemPatches;
125
+ directApi._itemPatches = null;
126
+ let node;
127
+ try {
128
+ node = runDirectCreate(Ctor, child);
129
+ }
130
+ finally {
131
+ directApi._itemPatches = prevItemPatches;
132
+ if (entered && adopt && typeof adopt.leave === 'function')
133
+ adopt.leave();
134
+ }
135
+ if (node) {
136
+ // ifBlock/eachBlock roots are DocumentFragments: appendChild moves
137
+ // children into `host` and empties the fragment. Keep a live Element
138
+ // root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
139
+ child.__vmzDomRoot = node.nodeType === 11 ? host : node;
140
+ host.appendChild(node);
141
+ }
142
+ host.__vmzInst = child;
143
+ if (typeof child.onMount === 'function') {
144
+ const pending = Promise.resolve().then(() => {
145
+ if (!child.__vmzDestroyed)
146
+ return child.onMount();
147
+ });
148
+ const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
149
+ bag.push(pending);
150
+ }
151
+ return host;
152
+ }
33
153
  export const directApi = {
34
154
  _inst: null,
35
155
  _branchBinds: null,
36
156
  _itemPatches: null,
37
157
  _eachCtx: null,
38
158
  _resumeAdopt: null,
39
- el(tag) {
159
+ el(tag, key) {
40
160
  const name = tag || 'div';
41
161
  const svg = inSvgScope() || name === 'svg';
162
+ const rowKey = key == null ? null : key;
42
163
  if (directApi._resumeAdopt)
43
- return directApi._resumeAdopt.el(name, svg);
164
+ return directApi._resumeAdopt.el(name, svg, rowKey);
44
165
  noteDomCreate();
45
- return svg ? document.createElementNS(SVG_NS, name) : document.createElement(name);
166
+ const node = svg ? document.createElementNS(SVG_NS, name) : document.createElement(name);
167
+ // Row keys live as expandos on the live DOM (the clone kernel does the same)
168
+ // and as `data-vmz-key` in SSR output for resume reclamation.
169
+ if (rowKey != null)
170
+ node.__vmzKey = rowKey;
171
+ return node;
46
172
  },
47
173
  elNS(ns, tag) {
48
174
  if (directApi._resumeAdopt && typeof directApi._resumeAdopt.elNS === 'function') {
@@ -57,6 +183,26 @@ export const directApi = {
57
183
  noteDomCreate();
58
184
  return document.createTextNode(value == null ? '' : String(value));
59
185
  },
186
+ /**
187
+ * Emitted `v-html` write. Resume parks the SSR children of a reclaimed host,
188
+ * so the live `innerHTML` is still empty when the first patch runs. SSR stamps
189
+ * `HTML_HASH_ATTR` with the digest of the exact payload string it wrote: a
190
+ * matching digest proves the parked subtree is the same payload, so adopt
191
+ * moves it back and no text is ever re-parsed. A different digest writes the
192
+ * new payload and refreshes the marker for later identical runs.
193
+ */
194
+ html(el, value) {
195
+ const next = value == null ? '' : String(value);
196
+ const digest = htmlDigest(next);
197
+ const adopt = directApi._resumeAdopt;
198
+ if (adopt && typeof adopt.reclaimHtml === 'function' && adopt.reclaimHtml(el, digest))
199
+ return;
200
+ const marker = typeof el.getAttribute === 'function' ? el.getAttribute(HTML_HASH_ATTR) : null;
201
+ if (el.innerHTML !== next)
202
+ el.innerHTML = next;
203
+ if (marker != null)
204
+ el.setAttribute(HTML_HASH_ATTR, digest);
205
+ },
60
206
  nsScopeDepth() {
61
207
  return nsStack.length;
62
208
  },
@@ -359,96 +505,87 @@ export const directApi = {
359
505
  /**
360
506
  * Nested component (sync Direct child or island schedule).
361
507
  */
362
- component(hostInst, nameOrCtor, props, client) {
363
- const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
508
+ component(hostInst, nameOrCtor, props, client, key) {
509
+ const name = componentDisplayName(nameOrCtor);
364
510
  let host = null;
365
511
  if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
366
- host = directApi._resumeAdopt.componentHost(name);
512
+ host = directApi._resumeAdopt.componentHost(name, key);
367
513
  }
368
514
  if (!host) {
369
515
  noteDomCreate();
370
516
  host = document.createElement('div');
371
517
  host.setAttribute('data-vmz', name);
372
518
  }
373
- const resolved = {};
374
- for (const [k, v] of Object.entries(props || {})) {
375
- // Function props that already look like `onXxx` stay as handlers.
376
- // Component `@event` never arrives here (emit uses onComponentEvent).
377
- const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
378
- if (onKey)
379
- resolved[onKey] = v;
380
- else if (typeof v === 'function')
381
- resolved[k] = v.call(hostInst);
382
- else
383
- resolved[k] = v;
384
- }
385
- if (client) {
386
- host.setAttribute('data-vmz-island', name);
387
- host.setAttribute('data-vmz-client', String(client));
388
- host.setAttribute('data-vmz-props', JSON.stringify(stripFns(resolved)));
389
- if (isEventEntryStrategy(String(client))) {
390
- host.setAttribute('data-vmz-entry', 'event');
391
- }
392
- // resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
393
- scheduleClientOn(host, String(client), async () => {
394
- const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : await resolveComponent(name);
395
- if (!Ctor) {
396
- // Replace placeholder island with leaf error node (do not throw page).
397
- const err = createUnknownComponentElement(name, 'island');
398
- host.replaceWith(err);
399
- return;
400
- }
401
- const { resume } = await import('../resume/resume.js');
402
- await resume(Ctor, host, { props: resolved, state: {} });
403
- });
404
- return host;
405
- }
406
- // Static Ctor import path: use Function directly (no registry lookup).
407
- const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : components[name];
408
- if (!Ctor) {
409
- return createUnknownComponentElement(name, 'client');
410
- }
411
- // Inline chips ??`display: contents` (`ui-direct-host-box`). Block surfaces
412
- // keep a real box (DataTable select timed out when defaulting everything).
413
- applyDirectHostBox(host, name, Ctor);
414
- const child = createInstance(Ctor, resolved);
415
- if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
416
- throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
417
- }
418
- // Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
419
- // Enter the host's private child pool so nested create cannot see uncle siblings.
420
- const adopt = directApi._resumeAdopt;
421
- const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
422
- // A child nested inside the parent's `v-for` must not feed the parent item
423
- // patch sink: those patches belong to the child instance (`this`) and the
424
- // parent's each replays them with the parent host, wiping child output.
425
- const prevItemPatches = directApi._itemPatches;
426
- directApi._itemPatches = null;
427
- let node;
428
- try {
429
- node = runDirectCreate(Ctor, child);
430
- }
431
- finally {
432
- directApi._itemPatches = prevItemPatches;
433
- if (entered && adopt && typeof adopt.leave === 'function')
434
- adopt.leave();
435
- }
436
- if (node) {
437
- // ifBlock/eachBlock roots are DocumentFragments: appendChild moves
438
- // children into `host` and empties the fragment. Keep a live Element
439
- // root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
440
- child.__vmzDomRoot = node.nodeType === 11 ? host : node;
441
- host.appendChild(node);
519
+ if (key != null)
520
+ host.__vmzKey = key;
521
+ return mountDirectComponentHost(host, hostInst, name, nameOrCtor, props, client);
522
+ },
523
+ /**
524
+ * `<component :is>` boundary. The host element is stable, the child instance
525
+ * is torn down and rebuilt whenever the evaluated `is` value changes. Props
526
+ * are re-evaluated per mount so the new type starts from current values, and
527
+ * the child listener bag is carried over so `@event` handlers keep working.
528
+ */
529
+ dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
530
+ const resolve = () => {
531
+ try {
532
+ return getCtor.call(hostInst);
533
+ }
534
+ catch {
535
+ return null;
536
+ }
537
+ };
538
+ const initial = resolve();
539
+ const name = componentDisplayName(initial);
540
+ let host = null;
541
+ if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
542
+ host = directApi._resumeAdopt.componentHost(name, key);
442
543
  }
443
- host.__vmzInst = child;
444
- if (typeof child.onMount === 'function') {
445
- const pending = Promise.resolve().then(() => {
446
- if (!child.__vmzDestroyed)
447
- return child.onMount();
448
- });
449
- const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
450
- bag.push(pending);
544
+ if (!host) {
545
+ noteDomCreate();
546
+ host = document.createElement('div');
547
+ host.setAttribute('data-vmz', name);
451
548
  }
549
+ if (key != null)
550
+ host.__vmzKey = key;
551
+ let current = initial;
552
+ const render = (next) => {
553
+ const old = host.__vmzInst;
554
+ let listeners = null;
555
+ if (old) {
556
+ listeners = old.__vmzComponentListeners || null;
557
+ destroy(old);
558
+ host.__vmzInst = null;
559
+ }
560
+ while (host.firstChild)
561
+ host.removeChild(host.firstChild);
562
+ current = next;
563
+ const nextName = componentDisplayName(next);
564
+ host.setAttribute('data-vmz', nextName);
565
+ if (!next)
566
+ return;
567
+ const Ctor = typeof next === 'function' ? next : components[nextName];
568
+ if (!Ctor)
569
+ return;
570
+ let props = {};
571
+ try {
572
+ props = getProps.call(hostInst) || {};
573
+ }
574
+ catch {
575
+ props = {};
576
+ }
577
+ mountDirectComponentHost(host, hostInst, nextName, Ctor, props, null);
578
+ const child = host.__vmzInst;
579
+ if (listeners && child)
580
+ child.__vmzComponentListeners = listeners;
581
+ };
582
+ render(initial);
583
+ directApi.trackPatch(hostInst, deps || [], function __vmzDynamicComponentPatch() {
584
+ const next = resolve();
585
+ if (next === current)
586
+ return;
587
+ render(next);
588
+ }, bindingId);
452
589
  return host;
453
590
  },
454
591
  /**
@@ -499,6 +636,206 @@ export const directApi = {
499
636
  scheduleRefresh(child, { type: 'replace', root: propName });
500
637
  }, bindingId);
501
638
  },
639
+ /**
640
+ * Object form of `bindComponentProp`: spread every key of the patch-time object
641
+ * across the child props and clear props whose keys disappear from the object.
642
+ */
643
+ bindComponentProps(hostInst, hostEl, slot, deps, get) {
644
+ if (hostEl && hostEl.__vmzPropBindSeq == null) {
645
+ hostEl.__vmzPropBindSeq = ++directPropBindSeq;
646
+ }
647
+ const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
648
+ const bindingId = `pc:${seq}:*${slot || 'obj'}`;
649
+ const seen = Object.create(null);
650
+ directApi.trackPatch(hostInst, deps || [], function bindComponentPropsPatch() {
651
+ let raw;
652
+ try {
653
+ raw = get.call(hostInst);
654
+ }
655
+ catch {
656
+ raw = null;
657
+ }
658
+ const child = hostEl && hostEl.__vmzInst;
659
+ if (!child || child.__vmzDestroyed)
660
+ return;
661
+ const next = Object.create(null);
662
+ if (raw && typeof raw === 'object') {
663
+ for (const key of Object.keys(raw)) {
664
+ if (!key || key.startsWith('#'))
665
+ continue;
666
+ next[key] = raw[key];
667
+ }
668
+ }
669
+ const notify = (key, value) => {
670
+ child[key] = value;
671
+ if (typeof child.__vmzOnParentProp === 'function') {
672
+ try {
673
+ child.__vmzOnParentProp(key, value);
674
+ }
675
+ catch (err) {
676
+ console.error('vmz:dom __vmzOnParentProp', err);
677
+ }
678
+ }
679
+ scheduleRefresh(child, { type: 'replace', root: key });
680
+ };
681
+ for (const key of Object.keys(seen)) {
682
+ if (key in next)
683
+ continue;
684
+ delete seen[key];
685
+ notify(key, undefined);
686
+ }
687
+ for (const key of Object.keys(next)) {
688
+ const value = next[key];
689
+ const had = key in seen;
690
+ const prev = seen[key];
691
+ seen[key] = value;
692
+ // Create-time `...api.spreadProps()` already delivered identical
693
+ // values, so an unchanged first pass must not schedule a refresh.
694
+ if (had && Object.is(prev, value))
695
+ continue;
696
+ if (!had && Object.is(child[key], value))
697
+ continue;
698
+ notify(key, value);
699
+ }
700
+ }, bindingId);
701
+ },
702
+ /**
703
+ * Object form of `onComponentEvent`: diff event names per call site and detach
704
+ * listeners whose keys disappear. No global `off` semantics — the bag entry
705
+ * belongs to this call site only.
706
+ */
707
+ bindComponentEvents(hostInst, hostEl, slot, deps, get) {
708
+ if (hostEl && hostEl.__vmzPropBindSeq == null) {
709
+ hostEl.__vmzPropBindSeq = ++directPropBindSeq;
710
+ }
711
+ const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
712
+ const attached = Object.create(null);
713
+ directApi.trackPatch(hostInst, deps || [], function bindComponentEventsPatch() {
714
+ let raw;
715
+ try {
716
+ raw = get.call(hostInst);
717
+ }
718
+ catch {
719
+ raw = null;
720
+ }
721
+ const child = hostEl && hostEl.__vmzInst;
722
+ if (!child || child.__vmzDestroyed)
723
+ return;
724
+ const next = Object.create(null);
725
+ if (raw && typeof raw === 'object') {
726
+ for (const key of Object.keys(raw)) {
727
+ if (!key || key.startsWith('#'))
728
+ continue;
729
+ const list = normalizeHandlerList(raw[key]);
730
+ if (list.length)
731
+ next[key] = list;
732
+ }
733
+ }
734
+ const bag = child.__vmzComponentListeners ||
735
+ (child.__vmzComponentListeners = Object.create(null));
736
+ for (const key of Object.keys(attached)) {
737
+ if (key in next)
738
+ continue;
739
+ const rec = attached[key];
740
+ delete attached[key];
741
+ const list = bag[key];
742
+ if (!Array.isArray(list))
743
+ continue;
744
+ const index = list.indexOf(rec.wrapper);
745
+ if (index >= 0)
746
+ list.splice(index, 1);
747
+ if (!list.length)
748
+ delete bag[key];
749
+ }
750
+ for (const key of Object.keys(next)) {
751
+ const handlers = next[key];
752
+ const rec = attached[key];
753
+ if (rec) {
754
+ rec.handlers = handlers;
755
+ continue;
756
+ }
757
+ const created = { handlers, wrapper: null };
758
+ created.wrapper = (...args) => {
759
+ for (const fn of created.handlers)
760
+ fn(...args);
761
+ };
762
+ (bag[key] || (bag[key] = [])).push(created.wrapper);
763
+ attached[key] = created;
764
+ }
765
+ }, `pcev:${seq}:${slot || 'obj'}`);
766
+ },
767
+ /**
768
+ * `v-bind="obj"` / `v-on="obj"` listeners scoped to one call site.
769
+ *
770
+ * `mode: 'bind'` reads `onXxx` keys out of a bind object, `mode: 'on'` reads
771
+ * event names directly. Vue lowercases element listener names, so both modes
772
+ * normalize before touching `addEventListener`.
773
+ */
774
+ bindObjectListeners(el, slot, source, inst, mode) {
775
+ if (!el || !slot)
776
+ return;
777
+ const bag = el.__vmzObjEvt || (el.__vmzObjEvt = Object.create(null));
778
+ const state = bag[slot] || (bag[slot] = { types: Object.create(null) });
779
+ const prev = state.types;
780
+ const next = Object.create(null);
781
+ if (source && typeof source === 'object') {
782
+ for (const key of Object.keys(source)) {
783
+ let type;
784
+ if (mode === 'bind') {
785
+ if (!/^on[A-Z]/.test(key))
786
+ continue;
787
+ type = key.slice(2).toLowerCase();
788
+ }
789
+ else {
790
+ type = String(key).toLowerCase();
791
+ }
792
+ const list = normalizeHandlerList(source[key]);
793
+ if (type && list.length)
794
+ next[type] = list;
795
+ }
796
+ }
797
+ for (const type of Object.keys(prev)) {
798
+ if (type in next)
799
+ continue;
800
+ el.removeEventListener(type, prev[type].wrapper);
801
+ delete prev[type];
802
+ }
803
+ for (const type of Object.keys(next)) {
804
+ const handlers = next[type];
805
+ const rec = prev[type];
806
+ if (rec) {
807
+ rec.handlers = handlers;
808
+ continue;
809
+ }
810
+ const created = { handlers, wrapper: null };
811
+ created.wrapper = (ev) => {
812
+ if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
813
+ ev.preventDefault();
814
+ }
815
+ runDomEventHandler(inst || null, null, () => {
816
+ for (const fn of created.handlers)
817
+ fn.call(inst || null, ev);
818
+ });
819
+ };
820
+ prev[type] = created;
821
+ el.addEventListener(type, created.wrapper);
822
+ }
823
+ },
824
+ /**
825
+ * Filter a `v-bind="obj"` source down to bindable keys before it reaches
826
+ * component props (`#slot` markers belong to the compiler, not the child).
827
+ */
828
+ spreadProps(source) {
829
+ const out = Object.create(null);
830
+ if (source && typeof source === 'object') {
831
+ for (const key of Object.keys(source)) {
832
+ if (!key || key.startsWith('#'))
833
+ continue;
834
+ out[key] = source[key];
835
+ }
836
+ }
837
+ return out;
838
+ },
502
839
  /**
503
840
  * Project parent children into nested Direct component default `<slot>`.
504
841
  * Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
@@ -8,6 +8,12 @@ export type ComponentCtor = ComponentCtorLike & {
8
8
  };
9
9
  export type VmzDomElement = Element & {
10
10
  __vmzEvt?: Record<string, EventListener>;
11
+ __vmzObjEvt?: Record<string, {
12
+ types: Record<string, {
13
+ handlers: Function[];
14
+ wrapper: EventListener;
15
+ }>;
16
+ }>;
11
17
  __vmzInst?: DirectInstance;
12
18
  __vmzPropBindSeq?: number;
13
19
  __vmzDispose?: (() => void) | null;
@@ -69,15 +75,17 @@ export type TraceBuffer = {
69
75
  events: TraceEvent[];
70
76
  };
71
77
  export type ResumeAdoptCtx = {
72
- el: (tag: string, svg?: boolean) => Element;
78
+ el: (tag: string, svg?: boolean, key?: unknown) => Element;
73
79
  elNS?: (ns: string, tag: string) => Element;
74
80
  text: (value?: unknown) => Text;
75
- componentHost?: (name: string) => HTMLElement | null;
81
+ componentHost?: (name: string, key?: unknown) => HTMLElement | null;
76
82
  enter?: (node: Element) => boolean;
77
83
  leave?: () => void;
78
84
  scopeDepth?: () => number;
79
85
  rewindScope?: (depth: number) => void;
80
86
  beginBranchScope?: () => () => void;
87
+ /** Keep the parked SSR subtree when its SSR payload digest equals `digest`. */
88
+ reclaimHtml?: (el: Element, digest: string) => boolean;
81
89
  _enterBalance?: number;
82
90
  inst?: DirectInstance;
83
91
  };