@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.
- package/dist/browser/client-nav.types.d.ts +2 -5
- package/dist/browser/direct-api.types.d.ts +6 -0
- package/dist/browser/dom/attributes.js +142 -2
- package/dist/browser/dom/direct-api.d.ts +43 -3
- package/dist/browser/dom/direct-api.js +421 -84
- package/dist/browser/dom-core.types.d.ts +10 -2
- package/dist/browser/navigation/client-navigation.js +18 -30
- package/dist/browser/reactivity/write-barrier.js +5 -1
- package/dist/browser/resume/adopt.js +142 -10
- package/dist/browser/resume/hydrate.js +34 -0
- package/dist/host/deployment/deployment-registry.js +5 -2
- package/dist/host/render/render-host.js +2 -0
- package/dist/host/serve/bootstrap.js +8 -4
- package/dist/host/serve/hmr-dev.d.ts +16 -17
- package/dist/host/serve/hmr-dev.js +96 -35
- package/dist/host/serve/http.js +25 -8
- package/dist/host/serve/pack-entry.d.ts +21 -0
- package/dist/host/serve/pack-entry.js +78 -7
- package/dist/host/serve/page-render.js +10 -8
- package/dist/shared/dom-attr-normalize.d.ts +6 -0
- package/dist/shared/dom-attr-normalize.js +1 -1
- package/dist/shared/host.types.d.ts +9 -0
- package/dist/shared/html-digest.d.ts +15 -0
- package/dist/shared/html-digest.js +25 -0
- package/dist/ssr/serialize.js +305 -9
- package/package.json +7 -7
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
};
|