@vmz/core 0.3.13 → 0.3.15

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 { applyDirectHostBox } from '../direct-host-box.js';
2
+ import { HTML_HASH_ATTR, htmlDigest } from '../../shared/html-digest.js';
3
+ import { applyDirectHostBox, resolveDirectHostTag } 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,52 @@ 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
+ let next;
196
+ if (value == null) {
197
+ // `null` is "this create has no payload yet", not "the payload is
198
+ // empty": a resumed host may still own the SSR payload, so hand the
199
+ // parked subtree back instead of clearing the server's markup. After
200
+ // the resume pass ends `_resumeAdopt` is gone and a null patch
201
+ // clears the host like Vue's `v-html` does.
202
+ const adopt = directApi._resumeAdopt;
203
+ if (adopt && typeof adopt.keepHtml === 'function' && adopt.keepHtml(el))
204
+ return;
205
+ // Already holding the SSR payload from an earlier null create: the
206
+ // create and its tracked patch both pass null, and only a real payload
207
+ // (or a host without one) may release the server's markup.
208
+ if (el.__vmzHtmlKeep)
209
+ return;
210
+ next = '';
211
+ }
212
+ else {
213
+ el.__vmzHtmlKeep = false;
214
+ next = String(value);
215
+ }
216
+ const digest = htmlDigest(next);
217
+ const adopt = directApi._resumeAdopt;
218
+ if (adopt && typeof adopt.reclaimHtml === 'function' && adopt.reclaimHtml(el, digest))
219
+ return;
220
+ // The browser serializer expands self-closing tags (`<path/>` becomes
221
+ // `<path></path>`), so `innerHTML` never equals the generated string for
222
+ // SVG payloads. Compare the payload digest instead: an equal stamp means
223
+ // this host already holds exactly this payload, so a repeated patch must
224
+ // keep the live nodes (identity, focus, and `ResizeObserver` targets).
225
+ if (typeof el.getAttribute === 'function' && el.getAttribute(HTML_HASH_ATTR) === digest)
226
+ return;
227
+ if (el.innerHTML !== next)
228
+ el.innerHTML = next;
229
+ if (typeof el.setAttribute === 'function')
230
+ el.setAttribute(HTML_HASH_ATTR, digest);
231
+ },
60
232
  nsScopeDepth() {
61
233
  return nsStack.length;
62
234
  },
@@ -76,6 +248,17 @@ export const directApi = {
76
248
  }
77
249
  return false;
78
250
  },
251
+ adoptEnterSlot(hostEl, name) {
252
+ nsStack.push(isSvgNode(hostEl));
253
+ const adopt = directApi._resumeAdopt;
254
+ if (adopt && typeof adopt.enterSlot === 'function') {
255
+ const ok = adopt.enterSlot(hostEl, name);
256
+ if (ok)
257
+ adopt._enterBalance = (adopt._enterBalance || 0) + 1;
258
+ return ok;
259
+ }
260
+ return false;
261
+ },
79
262
  adoptLeave() {
80
263
  nsStack.pop();
81
264
  const adopt = directApi._resumeAdopt;
@@ -93,6 +276,22 @@ export const directApi = {
93
276
  noteDomCreate();
94
277
  return document.createDocumentFragment();
95
278
  },
279
+ /**
280
+ * Multi-root `<template v-for>` row. Resume narrows adoption to the SSR
281
+ * marker pair for this key; a key with no pair gets an empty scope so a new
282
+ * row can never steal nodes that belong to another key.
283
+ */
284
+ fragRow(key) {
285
+ const adopt = directApi._resumeAdopt;
286
+ if (adopt && typeof adopt.rowStart === 'function')
287
+ adopt.rowStart(key);
288
+ return directApi.frag();
289
+ },
290
+ rowEnd() {
291
+ const adopt = directApi._resumeAdopt;
292
+ if (adopt && typeof adopt.rowEnd === 'function')
293
+ adopt.rowEnd();
294
+ },
96
295
  comment(value) {
97
296
  noteDomCreate();
98
297
  return document.createComment(value == null ? '' : String(value));
@@ -161,6 +360,15 @@ export const directApi = {
161
360
  disposeDomTree(root);
162
361
  },
163
362
  attr(el, name, value) {
363
+ // `v-pre`: the authored pair goes out untouched — no `className` → `class`
364
+ // rewrite, no class/style merge, no property special-casing.
365
+ if (el.__vmzVerbatim) {
366
+ if (value == null || value === false)
367
+ el.removeAttribute(name);
368
+ else
369
+ el.setAttribute(name, value === true ? '' : String(value));
370
+ return;
371
+ }
164
372
  applyDomAttr(el, name, value);
165
373
  },
166
374
  mergeClass(...parts) {
@@ -335,9 +543,11 @@ export const directApi = {
335
543
  raw = get.call(inst);
336
544
  }
337
545
  catch {
338
- raw = '';
546
+ raw = null;
339
547
  }
340
- el.innerHTML = raw == null ? '' : String(raw);
548
+ // Same payload contract as the emitted `v-html` call: digest
549
+ // matching and resume adoption must not be bypassed here.
550
+ directApi.html(el, raw);
341
551
  }, bindingId);
342
552
  },
343
553
  /**
@@ -359,96 +569,87 @@ export const directApi = {
359
569
  /**
360
570
  * Nested component (sync Direct child or island schedule).
361
571
  */
362
- component(hostInst, nameOrCtor, props, client) {
363
- const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
572
+ component(hostInst, nameOrCtor, props, client, key) {
573
+ const name = componentDisplayName(nameOrCtor);
364
574
  let host = null;
365
575
  if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
366
- host = directApi._resumeAdopt.componentHost(name);
576
+ host = directApi._resumeAdopt.componentHost(name, key);
367
577
  }
368
578
  if (!host) {
369
579
  noteDomCreate();
370
- host = document.createElement('div');
580
+ host = document.createElement(resolveDirectHostTag(name, nameOrCtor));
371
581
  host.setAttribute('data-vmz', name);
372
582
  }
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);
583
+ if (key != null)
584
+ host.__vmzKey = key;
585
+ return mountDirectComponentHost(host, hostInst, name, nameOrCtor, props, client);
586
+ },
587
+ /**
588
+ * `<component :is>` boundary. The host element is stable, the child instance
589
+ * is torn down and rebuilt whenever the evaluated `is` value changes. Props
590
+ * are re-evaluated per mount so the new type starts from current values, and
591
+ * the child listener bag is carried over so `@event` handlers keep working.
592
+ */
593
+ dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
594
+ const resolve = () => {
595
+ try {
596
+ return getCtor.call(hostInst);
597
+ }
598
+ catch {
599
+ return null;
600
+ }
601
+ };
602
+ const initial = resolve();
603
+ const name = componentDisplayName(initial);
604
+ let host = null;
605
+ if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
606
+ host = directApi._resumeAdopt.componentHost(name, key);
442
607
  }
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);
608
+ if (!host) {
609
+ noteDomCreate();
610
+ host = document.createElement(resolveDirectHostTag(name, initial));
611
+ host.setAttribute('data-vmz', name);
451
612
  }
613
+ if (key != null)
614
+ host.__vmzKey = key;
615
+ let current = initial;
616
+ const render = (next) => {
617
+ const old = host.__vmzInst;
618
+ let listeners = null;
619
+ if (old) {
620
+ listeners = old.__vmzComponentListeners || null;
621
+ destroy(old);
622
+ host.__vmzInst = null;
623
+ }
624
+ while (host.firstChild)
625
+ host.removeChild(host.firstChild);
626
+ current = next;
627
+ const nextName = componentDisplayName(next);
628
+ host.setAttribute('data-vmz', nextName);
629
+ if (!next)
630
+ return;
631
+ const Ctor = typeof next === 'function' ? next : components[nextName];
632
+ if (!Ctor)
633
+ return;
634
+ let props = {};
635
+ try {
636
+ props = getProps.call(hostInst) || {};
637
+ }
638
+ catch {
639
+ props = {};
640
+ }
641
+ mountDirectComponentHost(host, hostInst, nextName, Ctor, props, null);
642
+ const child = host.__vmzInst;
643
+ if (listeners && child)
644
+ child.__vmzComponentListeners = listeners;
645
+ };
646
+ render(initial);
647
+ directApi.trackPatch(hostInst, deps || [], function __vmzDynamicComponentPatch() {
648
+ const next = resolve();
649
+ if (next === current)
650
+ return;
651
+ render(next);
652
+ }, bindingId);
452
653
  return host;
453
654
  },
454
655
  /**
@@ -499,6 +700,206 @@ export const directApi = {
499
700
  scheduleRefresh(child, { type: 'replace', root: propName });
500
701
  }, bindingId);
501
702
  },
703
+ /**
704
+ * Object form of `bindComponentProp`: spread every key of the patch-time object
705
+ * across the child props and clear props whose keys disappear from the object.
706
+ */
707
+ bindComponentProps(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 bindingId = `pc:${seq}:*${slot || 'obj'}`;
713
+ const seen = Object.create(null);
714
+ directApi.trackPatch(hostInst, deps || [], function bindComponentPropsPatch() {
715
+ let raw;
716
+ try {
717
+ raw = get.call(hostInst);
718
+ }
719
+ catch {
720
+ raw = null;
721
+ }
722
+ const child = hostEl && hostEl.__vmzInst;
723
+ if (!child || child.__vmzDestroyed)
724
+ return;
725
+ const next = Object.create(null);
726
+ if (raw && typeof raw === 'object') {
727
+ for (const key of Object.keys(raw)) {
728
+ if (!key || key.startsWith('#'))
729
+ continue;
730
+ next[key] = raw[key];
731
+ }
732
+ }
733
+ const notify = (key, value) => {
734
+ child[key] = value;
735
+ if (typeof child.__vmzOnParentProp === 'function') {
736
+ try {
737
+ child.__vmzOnParentProp(key, value);
738
+ }
739
+ catch (err) {
740
+ console.error('vmz:dom __vmzOnParentProp', err);
741
+ }
742
+ }
743
+ scheduleRefresh(child, { type: 'replace', root: key });
744
+ };
745
+ for (const key of Object.keys(seen)) {
746
+ if (key in next)
747
+ continue;
748
+ delete seen[key];
749
+ notify(key, undefined);
750
+ }
751
+ for (const key of Object.keys(next)) {
752
+ const value = next[key];
753
+ const had = key in seen;
754
+ const prev = seen[key];
755
+ seen[key] = value;
756
+ // Create-time `...api.spreadProps()` already delivered identical
757
+ // values, so an unchanged first pass must not schedule a refresh.
758
+ if (had && Object.is(prev, value))
759
+ continue;
760
+ if (!had && Object.is(child[key], value))
761
+ continue;
762
+ notify(key, value);
763
+ }
764
+ }, bindingId);
765
+ },
766
+ /**
767
+ * Object form of `onComponentEvent`: diff event names per call site and detach
768
+ * listeners whose keys disappear. No global `off` semantics — the bag entry
769
+ * belongs to this call site only.
770
+ */
771
+ bindComponentEvents(hostInst, hostEl, slot, deps, get) {
772
+ if (hostEl && hostEl.__vmzPropBindSeq == null) {
773
+ hostEl.__vmzPropBindSeq = ++directPropBindSeq;
774
+ }
775
+ const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
776
+ const attached = Object.create(null);
777
+ directApi.trackPatch(hostInst, deps || [], function bindComponentEventsPatch() {
778
+ let raw;
779
+ try {
780
+ raw = get.call(hostInst);
781
+ }
782
+ catch {
783
+ raw = null;
784
+ }
785
+ const child = hostEl && hostEl.__vmzInst;
786
+ if (!child || child.__vmzDestroyed)
787
+ return;
788
+ const next = Object.create(null);
789
+ if (raw && typeof raw === 'object') {
790
+ for (const key of Object.keys(raw)) {
791
+ if (!key || key.startsWith('#'))
792
+ continue;
793
+ const list = normalizeHandlerList(raw[key]);
794
+ if (list.length)
795
+ next[key] = list;
796
+ }
797
+ }
798
+ const bag = child.__vmzComponentListeners ||
799
+ (child.__vmzComponentListeners = Object.create(null));
800
+ for (const key of Object.keys(attached)) {
801
+ if (key in next)
802
+ continue;
803
+ const rec = attached[key];
804
+ delete attached[key];
805
+ const list = bag[key];
806
+ if (!Array.isArray(list))
807
+ continue;
808
+ const index = list.indexOf(rec.wrapper);
809
+ if (index >= 0)
810
+ list.splice(index, 1);
811
+ if (!list.length)
812
+ delete bag[key];
813
+ }
814
+ for (const key of Object.keys(next)) {
815
+ const handlers = next[key];
816
+ const rec = attached[key];
817
+ if (rec) {
818
+ rec.handlers = handlers;
819
+ continue;
820
+ }
821
+ const created = { handlers, wrapper: null };
822
+ created.wrapper = (...args) => {
823
+ for (const fn of created.handlers)
824
+ fn(...args);
825
+ };
826
+ (bag[key] || (bag[key] = [])).push(created.wrapper);
827
+ attached[key] = created;
828
+ }
829
+ }, `pcev:${seq}:${slot || 'obj'}`);
830
+ },
831
+ /**
832
+ * `v-bind="obj"` / `v-on="obj"` listeners scoped to one call site.
833
+ *
834
+ * `mode: 'bind'` reads `onXxx` keys out of a bind object, `mode: 'on'` reads
835
+ * event names directly. Vue lowercases element listener names, so both modes
836
+ * normalize before touching `addEventListener`.
837
+ */
838
+ bindObjectListeners(el, slot, source, inst, mode) {
839
+ if (!el || !slot)
840
+ return;
841
+ const bag = el.__vmzObjEvt || (el.__vmzObjEvt = Object.create(null));
842
+ const state = bag[slot] || (bag[slot] = { types: Object.create(null) });
843
+ const prev = state.types;
844
+ const next = Object.create(null);
845
+ if (source && typeof source === 'object') {
846
+ for (const key of Object.keys(source)) {
847
+ let type;
848
+ if (mode === 'bind') {
849
+ if (!/^on[A-Z]/.test(key))
850
+ continue;
851
+ type = key.slice(2).toLowerCase();
852
+ }
853
+ else {
854
+ type = String(key).toLowerCase();
855
+ }
856
+ const list = normalizeHandlerList(source[key]);
857
+ if (type && list.length)
858
+ next[type] = list;
859
+ }
860
+ }
861
+ for (const type of Object.keys(prev)) {
862
+ if (type in next)
863
+ continue;
864
+ el.removeEventListener(type, prev[type].wrapper);
865
+ delete prev[type];
866
+ }
867
+ for (const type of Object.keys(next)) {
868
+ const handlers = next[type];
869
+ const rec = prev[type];
870
+ if (rec) {
871
+ rec.handlers = handlers;
872
+ continue;
873
+ }
874
+ const created = { handlers, wrapper: null };
875
+ created.wrapper = (ev) => {
876
+ if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
877
+ ev.preventDefault();
878
+ }
879
+ runDomEventHandler(inst || null, null, () => {
880
+ for (const fn of created.handlers)
881
+ fn.call(inst || null, ev);
882
+ });
883
+ };
884
+ prev[type] = created;
885
+ el.addEventListener(type, created.wrapper);
886
+ }
887
+ },
888
+ /**
889
+ * Filter a `v-bind="obj"` source down to bindable keys before it reaches
890
+ * component props (`#slot` markers belong to the compiler, not the child).
891
+ */
892
+ spreadProps(source) {
893
+ const out = Object.create(null);
894
+ if (source && typeof source === 'object') {
895
+ for (const key of Object.keys(source)) {
896
+ if (!key || key.startsWith('#'))
897
+ continue;
898
+ out[key] = source[key];
899
+ }
900
+ }
901
+ return out;
902
+ },
502
903
  /**
503
904
  * Project parent children into nested Direct component default `<slot>`.
504
905
  * Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.