@vmz/core 0.0.3 → 0.0.4
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/client-nav.d.ts +46 -0
- package/dist/client-nav.js +186 -0
- package/dist/dom.d.ts +21 -2
- package/dist/dom.js +1502 -180
- package/dist/serve-host.mjs +544 -88
- package/dist/server.js +97 -11
- package/package.json +5 -1
package/dist/dom.js
CHANGED
|
@@ -115,7 +115,7 @@ export function __vmzPrecisionSnapshot() {
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* @param { => any} fn
|
|
118
|
+
* @param {() => any} fn
|
|
119
119
|
* @param {string | null} [depKey]
|
|
120
120
|
* @param {number | string | null} [bindingId]
|
|
121
121
|
*/
|
|
@@ -166,16 +166,23 @@ async function resolveComponent(name) {
|
|
|
166
166
|
* @param {new (props?: object) => any} Component
|
|
167
167
|
* @param {object} [props]
|
|
168
168
|
*/
|
|
169
|
-
export async function renderToString(Component, props = {}) {
|
|
169
|
+
export async function renderToString(Component, props = {}, opts = {}) {
|
|
170
|
+
const signal = opts && opts.signal;
|
|
171
|
+
if (signal && signal.aborted)
|
|
172
|
+
return '';
|
|
170
173
|
const inst = createInstance(Component, props);
|
|
171
174
|
if (typeof inst.onMount === 'function') {
|
|
172
175
|
await inst.onMount();
|
|
173
176
|
}
|
|
177
|
+
if (signal && signal.aborted)
|
|
178
|
+
return '';
|
|
174
179
|
// production Direct emit: SSR only via Direct serialize schedule — never `render`.
|
|
175
180
|
if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
|
|
176
181
|
throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
177
182
|
}
|
|
178
183
|
const root = await runDirectSerializeTreeWithMounts(Component, inst);
|
|
184
|
+
if (opts && opts.slotHtml != null)
|
|
185
|
+
injectDefaultSlotHtml(root, opts.slotHtml);
|
|
179
186
|
return flattenSerializeNode(root);
|
|
180
187
|
}
|
|
181
188
|
/**
|
|
@@ -184,7 +191,7 @@ export async function renderToString(Component, props = {}) {
|
|
|
184
191
|
* Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
|
|
185
192
|
* @param {new (props?: object) => any} Component
|
|
186
193
|
* @param {object} [props]
|
|
187
|
-
* @param {{ signal?: AbortSignal }} [opts]
|
|
194
|
+
* @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
|
|
188
195
|
* @returns {AsyncGenerator<string, void, void>}
|
|
189
196
|
*/
|
|
190
197
|
export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
@@ -203,6 +210,8 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
|
203
210
|
throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
204
211
|
}
|
|
205
212
|
const root = await runDirectSerializeTreeWithMounts(Component, inst);
|
|
213
|
+
if (opts && opts.slotHtml != null)
|
|
214
|
+
injectDefaultSlotHtml(root, opts.slotHtml);
|
|
206
215
|
if (aborted())
|
|
207
216
|
return;
|
|
208
217
|
for (const chunk of streamSerializeChunks(root)) {
|
|
@@ -218,6 +227,111 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
|
218
227
|
destroy(inst);
|
|
219
228
|
}
|
|
220
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Fill the layout-owned default `<slot>` with pre-rendered HTML (layout SSR wrap).
|
|
232
|
+
* Skips nested component hosts (`data-vmz`) — their slots are for child projection
|
|
233
|
+
* (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle→Button
|
|
234
|
+
* before Layout's `<main><slot>`, and the entire page HTML lands inside a button.
|
|
235
|
+
* @param {any} node
|
|
236
|
+
* @param {string} html
|
|
237
|
+
*/
|
|
238
|
+
function injectDefaultSlotHtml(node, html) {
|
|
239
|
+
if (!node || typeof node !== 'object')
|
|
240
|
+
return false;
|
|
241
|
+
if (node.__kind === 'el' && node.tag === 'slot' && !(node.attrs && node.attrs.name)) {
|
|
242
|
+
node.__rawHtml = String(html ?? '');
|
|
243
|
+
node.children = [];
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
// Nested Direct component wrapper from serializeApi.component — do not search inside.
|
|
247
|
+
if (node.__kind === 'el' && node.attrs && node.attrs['data-vmz'] != null) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const kids = node.children;
|
|
251
|
+
if (Array.isArray(kids)) {
|
|
252
|
+
for (const c of kids) {
|
|
253
|
+
if (injectDefaultSlotHtml(c, html))
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
261
|
+
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
262
|
+
* @param {Element | null | undefined} root
|
|
263
|
+
* @returns {Element | null}
|
|
264
|
+
*/
|
|
265
|
+
export function findOwnedDefaultSlot(root) {
|
|
266
|
+
if (!root || root.nodeType !== 1)
|
|
267
|
+
return null;
|
|
268
|
+
const tag = String(root.tagName || '').toLowerCase();
|
|
269
|
+
if (tag === 'slot' && !root.getAttribute('name'))
|
|
270
|
+
return root;
|
|
271
|
+
const kids = root.children;
|
|
272
|
+
if (!kids || !kids.length)
|
|
273
|
+
return null;
|
|
274
|
+
for (let i = 0; i < kids.length; i++) {
|
|
275
|
+
const c = kids[i];
|
|
276
|
+
if (c.nodeType !== 1)
|
|
277
|
+
continue;
|
|
278
|
+
// Nested component host — its slots are not the layout page outlet.
|
|
279
|
+
if (c.hasAttribute('data-vmz'))
|
|
280
|
+
continue;
|
|
281
|
+
const hit = findOwnedDefaultSlot(c);
|
|
282
|
+
if (hit)
|
|
283
|
+
return hit;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
289
|
+
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
|
290
|
+
* outlet for the next layout or the page. Retains layout instances on `container`
|
|
291
|
+
* so SPA transitions can dispose only the page host.
|
|
292
|
+
* @param {new (props?: object) => any} Page
|
|
293
|
+
* @param {Element} container
|
|
294
|
+
* @param {object} [props]
|
|
295
|
+
* @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
|
|
296
|
+
* @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
|
|
297
|
+
*/
|
|
298
|
+
export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
|
|
299
|
+
if (typeof document === 'undefined') {
|
|
300
|
+
throw new Error('vmz:dom hydrateRoute() requires a document (browser)');
|
|
301
|
+
}
|
|
302
|
+
if (container.__vmzInst) {
|
|
303
|
+
destroy(container.__vmzInst);
|
|
304
|
+
container.__vmzInst = null;
|
|
305
|
+
}
|
|
306
|
+
container.__vmzPageHost = null;
|
|
307
|
+
container.__vmzLayoutInsts = null;
|
|
308
|
+
/** @type {object[]} */
|
|
309
|
+
const layoutInsts = [];
|
|
310
|
+
let host = container;
|
|
311
|
+
const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
|
|
312
|
+
for (const Layout of ctors) {
|
|
313
|
+
const inst = await mount(Layout, host, {});
|
|
314
|
+
layoutInsts.push(inst);
|
|
315
|
+
const slot = findOwnedDefaultSlot(inst.__vmzDomRoot);
|
|
316
|
+
const outlet = document.createElement('div');
|
|
317
|
+
outlet.setAttribute('data-vmz-outlet', '');
|
|
318
|
+
if (slot && slot.parentNode)
|
|
319
|
+
slot.replaceWith(outlet);
|
|
320
|
+
else if (inst.__vmzDomRoot && typeof inst.__vmzDomRoot.appendChild === 'function') {
|
|
321
|
+
inst.__vmzDomRoot.appendChild(outlet);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
host.appendChild(outlet);
|
|
325
|
+
}
|
|
326
|
+
host = outlet;
|
|
327
|
+
}
|
|
328
|
+
const pageInst = await hydrate(Page, host, props, opts);
|
|
329
|
+
container.__vmzPageHost = host;
|
|
330
|
+
container.__vmzLayoutInsts = layoutInsts;
|
|
331
|
+
// Outer layout (or page if no layouts) owns the #app instance for destroy().
|
|
332
|
+
container.__vmzInst = layoutInsts[0] || pageInst;
|
|
333
|
+
return pageInst;
|
|
334
|
+
}
|
|
221
335
|
/**
|
|
222
336
|
* Mount once; later updates are dep patches only (never re-run structure).
|
|
223
337
|
* Requires compiler `__vmzCreate` (production Direct emit — no blueprint fallback).
|
|
@@ -288,12 +402,25 @@ async function createFromComponent(Component, inst) {
|
|
|
288
402
|
* @param {object} inst
|
|
289
403
|
*/
|
|
290
404
|
function runDirectCreate(Component, inst) {
|
|
405
|
+
// Nested component creates (e.g. Button inside parent ifBlock branch) must not
|
|
406
|
+
// leak bindAttr/bindText into the parent's `_branchBinds` / `_itemPatches` sink —
|
|
407
|
+
// that steals numeric BindingIds (0) and corrupts parent deps (density → type).
|
|
408
|
+
const prevInst = directApi._inst;
|
|
409
|
+
const prevBranch = directApi._branchBinds;
|
|
410
|
+
const prevItems = directApi._itemPatches;
|
|
411
|
+
const prevEach = directApi._eachCtx;
|
|
291
412
|
directApi._inst = inst;
|
|
413
|
+
directApi._branchBinds = null;
|
|
414
|
+
directApi._itemPatches = null;
|
|
415
|
+
directApi._eachCtx = null;
|
|
292
416
|
try {
|
|
293
417
|
return Component.__vmzCreate.call(inst, directApi);
|
|
294
418
|
}
|
|
295
419
|
finally {
|
|
296
|
-
directApi._inst =
|
|
420
|
+
directApi._inst = prevInst;
|
|
421
|
+
directApi._branchBinds = prevBranch;
|
|
422
|
+
directApi._itemPatches = prevItems;
|
|
423
|
+
directApi._eachCtx = prevEach;
|
|
297
424
|
}
|
|
298
425
|
}
|
|
299
426
|
/**
|
|
@@ -369,6 +496,8 @@ function flattenSerializeNode(node) {
|
|
|
369
496
|
if (node.__kind === 'el') {
|
|
370
497
|
const tag = node.tag || 'div';
|
|
371
498
|
if (tag === 'slot') {
|
|
499
|
+
if (node.__rawHtml != null)
|
|
500
|
+
return String(node.__rawHtml);
|
|
372
501
|
return (node.children || []).map(flattenSerializeNode).join('');
|
|
373
502
|
}
|
|
374
503
|
const { open } = serializeOpenTag(node);
|
|
@@ -404,6 +533,10 @@ function* streamSerializeChunks(node) {
|
|
|
404
533
|
if (node.__kind === 'el') {
|
|
405
534
|
const tag = node.tag || 'div';
|
|
406
535
|
if (tag === 'slot') {
|
|
536
|
+
if (node.__rawHtml != null) {
|
|
537
|
+
yield String(node.__rawHtml);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
407
540
|
for (const c of node.children || [])
|
|
408
541
|
yield* streamSerializeChunks(c);
|
|
409
542
|
return;
|
|
@@ -476,10 +609,7 @@ const serializeApi = {
|
|
|
476
609
|
attr(el, name, value) {
|
|
477
610
|
if (!el || el.__kind !== 'el')
|
|
478
611
|
return;
|
|
479
|
-
|
|
480
|
-
delete el.attrs[name];
|
|
481
|
-
else
|
|
482
|
-
el.attrs[name] = String(value);
|
|
612
|
+
applySerializeAttr(el, name, value);
|
|
483
613
|
},
|
|
484
614
|
on() {
|
|
485
615
|
/* events are no-ops during SSR */
|
|
@@ -502,11 +632,50 @@ const serializeApi = {
|
|
|
502
632
|
catch {
|
|
503
633
|
raw = null;
|
|
504
634
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
635
|
+
applySerializeAttr(el, name, raw);
|
|
636
|
+
},
|
|
637
|
+
bindComponentProp() {
|
|
638
|
+
/* SSR: props already resolved into the child instance at create */
|
|
639
|
+
},
|
|
640
|
+
projectDefaultSlot(hostEl, node) {
|
|
641
|
+
if (!hostEl || node == null)
|
|
642
|
+
return;
|
|
643
|
+
// serializeApi.component returns a serialize el tree (or island shell).
|
|
644
|
+
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
645
|
+
const findSlot = (n) => {
|
|
646
|
+
if (!n || n.__kind !== 'el')
|
|
647
|
+
return null;
|
|
648
|
+
if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
|
|
649
|
+
return n;
|
|
650
|
+
for (const c of n.children || []) {
|
|
651
|
+
const hit = findSlot(c);
|
|
652
|
+
if (hit)
|
|
653
|
+
return hit;
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
};
|
|
657
|
+
// Prefer searching the component body (first child of host wrapper).
|
|
658
|
+
let slot = null;
|
|
659
|
+
if (root) {
|
|
660
|
+
for (const c of root.children || []) {
|
|
661
|
+
slot = findSlot(c);
|
|
662
|
+
if (slot)
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
if (!slot)
|
|
666
|
+
slot = findSlot(root);
|
|
667
|
+
}
|
|
668
|
+
if (slot) {
|
|
669
|
+
slot.__rawHtml = null;
|
|
670
|
+
if (!Array.isArray(slot.children))
|
|
671
|
+
slot.children = [];
|
|
672
|
+
// Append — multiple projectDefaultSlot calls must accumulate (SSR).
|
|
673
|
+
// Client path replaces the live <slot> then appends siblings; serialize must push.
|
|
674
|
+
slot.children.push(node);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (root)
|
|
678
|
+
root.appendChild(node);
|
|
510
679
|
},
|
|
511
680
|
setHtml(el, value) {
|
|
512
681
|
if (!el || el.__kind !== 'el')
|
|
@@ -584,7 +753,7 @@ const serializeApi = {
|
|
|
584
753
|
}
|
|
585
754
|
const dom = spec.createItem.call(inst, serializeApi, box);
|
|
586
755
|
if (dom) {
|
|
587
|
-
//
|
|
756
|
+
// SSR only: serialize key into HTML for hydrate/debug. Direct client does not write this attr.
|
|
588
757
|
if (dom.__kind === 'el')
|
|
589
758
|
serializeApi.attr(dom, 'data-vmz-key', String(k));
|
|
590
759
|
frag.appendChild(dom);
|
|
@@ -706,10 +875,7 @@ const directApi = {
|
|
|
706
875
|
return document.createDocumentFragment();
|
|
707
876
|
},
|
|
708
877
|
attr(el, name, value) {
|
|
709
|
-
|
|
710
|
-
el.removeAttribute(name);
|
|
711
|
-
else
|
|
712
|
-
el.setAttribute(name, String(value));
|
|
878
|
+
applyDomAttr(el, name, value);
|
|
713
879
|
},
|
|
714
880
|
on(el, type, handler) {
|
|
715
881
|
const inst = directApi._inst;
|
|
@@ -733,7 +899,7 @@ const directApi = {
|
|
|
733
899
|
* @param {object} inst
|
|
734
900
|
* @param {number|string|null} bindingId
|
|
735
901
|
* @param {string[]} deps
|
|
736
|
-
* @param { => any} get
|
|
902
|
+
* @param {() => any} get
|
|
737
903
|
* @param {Text} textNode
|
|
738
904
|
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
739
905
|
*/
|
|
@@ -746,7 +912,7 @@ const directApi = {
|
|
|
746
912
|
* @param {object} inst
|
|
747
913
|
* @param {number|string|null} bindingId
|
|
748
914
|
* @param {string[]} deps
|
|
749
|
-
* @param { => any} get
|
|
915
|
+
* @param {() => any} get
|
|
750
916
|
* @param {Element} el
|
|
751
917
|
* @param {string} name
|
|
752
918
|
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
@@ -754,13 +920,14 @@ const directApi = {
|
|
|
754
920
|
bindAttr(inst, bindingId, deps, get, el, name, cf) {
|
|
755
921
|
wireDirectBind(inst, bindingId, deps, get, (raw) => {
|
|
756
922
|
if (name === 'class' || name === 'className') {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
el.
|
|
923
|
+
const s = String(raw ?? '');
|
|
924
|
+
if (s)
|
|
925
|
+
el.setAttribute('class', s);
|
|
926
|
+
else if (el.hasAttribute('class'))
|
|
927
|
+
el.removeAttribute('class');
|
|
761
928
|
}
|
|
762
929
|
else {
|
|
763
|
-
el
|
|
930
|
+
applyDomAttr(el, name, raw);
|
|
764
931
|
}
|
|
765
932
|
}, cf);
|
|
766
933
|
},
|
|
@@ -772,7 +939,7 @@ const directApi = {
|
|
|
772
939
|
* @param {object} inst
|
|
773
940
|
* @param {number|string|null} bindingId
|
|
774
941
|
* @param {string[]} deps
|
|
775
|
-
* @param { => any} get
|
|
942
|
+
* @param {() => any} get
|
|
776
943
|
* @param {Element} el
|
|
777
944
|
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
778
945
|
*/
|
|
@@ -841,6 +1008,62 @@ const directApi = {
|
|
|
841
1008
|
}
|
|
842
1009
|
return host;
|
|
843
1010
|
},
|
|
1011
|
+
/**
|
|
1012
|
+
* Keep nested Direct child props live with parent field writes.
|
|
1013
|
+
* @param {object} hostInst
|
|
1014
|
+
* @param {HTMLElement} hostEl
|
|
1015
|
+
* @param {string} propName
|
|
1016
|
+
* @param {string[]} deps
|
|
1017
|
+
* @param {() => any} get
|
|
1018
|
+
*/
|
|
1019
|
+
bindComponentProp(hostInst, hostEl, propName, deps, get) {
|
|
1020
|
+
// Use a stable BindingId so flushPending schedules this patch via the IR
|
|
1021
|
+
// path. A null bindingId only lands in `__vmzBinders` and was skipped when
|
|
1022
|
+
// the same parent field also had bindText/bindAttr BindingIds.
|
|
1023
|
+
if (hostEl && hostEl.__vmzPropBindSeq == null) {
|
|
1024
|
+
hostEl.__vmzPropBindSeq = ++directPropBindSeq;
|
|
1025
|
+
}
|
|
1026
|
+
const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
|
|
1027
|
+
const bindingId = `pc:${seq}:${propName}`;
|
|
1028
|
+
wireDirectBind(hostInst, bindingId, deps, get, (raw) => {
|
|
1029
|
+
const child = hostEl && hostEl.__vmzInst;
|
|
1030
|
+
if (!child || child.__vmzDestroyed)
|
|
1031
|
+
return;
|
|
1032
|
+
if (typeof propName !== 'string' || !propName || propName.startsWith('#'))
|
|
1033
|
+
return;
|
|
1034
|
+
child[propName] = raw;
|
|
1035
|
+
scheduleRefresh(child, { type: 'replace', root: propName });
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
/**
|
|
1039
|
+
* Project parent children into nested Direct component default `<slot>`.
|
|
1040
|
+
* @param {HTMLElement} hostEl
|
|
1041
|
+
* @param {Node} node
|
|
1042
|
+
*/
|
|
1043
|
+
projectDefaultSlot(hostEl, node) {
|
|
1044
|
+
if (!hostEl || node == null)
|
|
1045
|
+
return;
|
|
1046
|
+
const child = hostEl.__vmzInst;
|
|
1047
|
+
const root = (child && child.__vmzDomRoot) || hostEl;
|
|
1048
|
+
/** @type {Element | null} */
|
|
1049
|
+
let slot = null;
|
|
1050
|
+
if (root && root.nodeType === 1) {
|
|
1051
|
+
if (String(root.tagName || '').toLowerCase() === 'slot' && !root.getAttribute('name')) {
|
|
1052
|
+
slot = root;
|
|
1053
|
+
}
|
|
1054
|
+
else if (typeof root.querySelector === 'function') {
|
|
1055
|
+
slot = root.querySelector('slot:not([name])');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (slot && slot.parentNode) {
|
|
1059
|
+
slot.replaceWith(node);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (root && typeof root.appendChild === 'function')
|
|
1063
|
+
root.appendChild(node);
|
|
1064
|
+
else
|
|
1065
|
+
hostEl.appendChild(node);
|
|
1066
|
+
},
|
|
844
1067
|
/**
|
|
845
1068
|
* Direct if/else — no blueprint `kind: "if"` dispatch.
|
|
846
1069
|
* @param {object} inst
|
|
@@ -958,7 +1181,7 @@ const directApi = {
|
|
|
958
1181
|
},
|
|
959
1182
|
/**
|
|
960
1183
|
* Direct keyed each — no blueprint `kind: "each"` dispatch.
|
|
961
|
-
* /: Set/Map + Fragment batch insert; item-local binds; host
|
|
1184
|
+
* /: Set/Map + Fragment batch insert; item-local binds; host field dispatch; event delegate.
|
|
962
1185
|
* @param {object} inst
|
|
963
1186
|
* @param {number|string|null} bindingId
|
|
964
1187
|
* @param {string[]} deps
|
|
@@ -989,6 +1212,9 @@ const directApi = {
|
|
|
989
1212
|
/** @type {Element | null} */
|
|
990
1213
|
let delegateRoot = null;
|
|
991
1214
|
const itemKey = (box) => {
|
|
1215
|
+
if (rowKeyField != null && box && box.item != null) {
|
|
1216
|
+
return box.item[rowKeyField];
|
|
1217
|
+
}
|
|
992
1218
|
if (typeof spec.key === 'function') {
|
|
993
1219
|
try {
|
|
994
1220
|
return spec.key.call(inst, box);
|
|
@@ -999,6 +1225,13 @@ const directApi = {
|
|
|
999
1225
|
}
|
|
1000
1226
|
return box.index;
|
|
1001
1227
|
};
|
|
1228
|
+
/** Reused for keyed lookups — avoid per-row `{item,index}` alloc on create/update. */
|
|
1229
|
+
const keyScratch = { item: null, index: 0 };
|
|
1230
|
+
const keyOf = (item, index) => {
|
|
1231
|
+
keyScratch.item = item;
|
|
1232
|
+
keyScratch.index = index;
|
|
1233
|
+
return itemKey(keyScratch);
|
|
1234
|
+
};
|
|
1002
1235
|
const readList = () => {
|
|
1003
1236
|
let list = [];
|
|
1004
1237
|
try {
|
|
@@ -1012,11 +1245,31 @@ const directApi = {
|
|
|
1012
1245
|
return list;
|
|
1013
1246
|
};
|
|
1014
1247
|
const runEntryPatches = (entry, depKey, onlyBindingId) => {
|
|
1015
|
-
if (!entry
|
|
1248
|
+
if (!entry)
|
|
1249
|
+
return;
|
|
1250
|
+
if (entryIsBp(entry) && applyBp) {
|
|
1251
|
+
if (onlyBindingId != null && blueprintBindIds && !blueprintBindIds.has(String(onlyBindingId))) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
applyBp(entry);
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
console.error('vmz:dom each item', err);
|
|
1259
|
+
}
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!entry.patches)
|
|
1016
1263
|
return;
|
|
1017
1264
|
for (const p of entry.patches) {
|
|
1018
|
-
if (onlyBindingId != null
|
|
1019
|
-
|
|
1265
|
+
if (onlyBindingId != null) {
|
|
1266
|
+
if (p.__vmzBindingIds) {
|
|
1267
|
+
if (!p.__vmzBindingIds.has(String(onlyBindingId)))
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
else if (p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1020
1273
|
}
|
|
1021
1274
|
try {
|
|
1022
1275
|
runPatch(p, depKey, onlyBindingId);
|
|
@@ -1032,14 +1285,21 @@ const directApi = {
|
|
|
1032
1285
|
const runAt = (i) => {
|
|
1033
1286
|
if (i < 0 || i >= list.length)
|
|
1034
1287
|
return;
|
|
1035
|
-
const
|
|
1036
|
-
const k =
|
|
1288
|
+
const item = list[i];
|
|
1289
|
+
const k = rowKeyOf(item, i);
|
|
1037
1290
|
const entry = keyed.get(k);
|
|
1038
1291
|
if (!entry)
|
|
1039
1292
|
return;
|
|
1040
|
-
entry.
|
|
1041
|
-
|
|
1042
|
-
|
|
1293
|
+
if (entry.nodeType === 1)
|
|
1294
|
+
entry.__vmzBox = item;
|
|
1295
|
+
else {
|
|
1296
|
+
entry.item = item;
|
|
1297
|
+
entry.index = i;
|
|
1298
|
+
if (entry.dom && entry.bp)
|
|
1299
|
+
entry.dom.__vmzBox = item;
|
|
1300
|
+
}
|
|
1301
|
+
if (entry.patches)
|
|
1302
|
+
tagItemPatches(entry.patches, i);
|
|
1043
1303
|
runEntryPatches(entry, (leafDeps && leafDeps[0]) || null, onlyBindingId);
|
|
1044
1304
|
};
|
|
1045
1305
|
if (allowIdx) {
|
|
@@ -1079,11 +1339,16 @@ const directApi = {
|
|
|
1079
1339
|
return;
|
|
1080
1340
|
const trie = inst.__vmzFlushTrie;
|
|
1081
1341
|
const hostFields = [];
|
|
1342
|
+
let listReplaced = false;
|
|
1082
1343
|
for (const d of leafDeps || []) {
|
|
1083
1344
|
if (!d)
|
|
1084
1345
|
continue;
|
|
1085
|
-
if (d.includes('.*') || (d.includes('[') && d.includes(']')))
|
|
1346
|
+
if (d.includes('.*') || (d.includes('[') && d.includes(']'))) {
|
|
1347
|
+
const root = depRootField(d);
|
|
1348
|
+
if (trie && root && trie[root] && trie[root].replace)
|
|
1349
|
+
listReplaced = true;
|
|
1086
1350
|
continue;
|
|
1351
|
+
}
|
|
1087
1352
|
hostFields.push(depRootField(d) || d);
|
|
1088
1353
|
}
|
|
1089
1354
|
const hostDirty = !!trie &&
|
|
@@ -1091,6 +1356,9 @@ const directApi = {
|
|
|
1091
1356
|
const n = trie[f];
|
|
1092
1357
|
return n && (n.replace || n.dirty);
|
|
1093
1358
|
});
|
|
1359
|
+
// Full list replace is owned by eachBlock apply() — skip leaf re-walk.
|
|
1360
|
+
if (listReplaced && !hostDirty)
|
|
1361
|
+
return;
|
|
1094
1362
|
if (hostDirty && hostFields.length) {
|
|
1095
1363
|
refreshHostKeyed(hostFields, bId);
|
|
1096
1364
|
return;
|
|
@@ -1156,9 +1424,16 @@ const directApi = {
|
|
|
1156
1424
|
let n = /** @type {Node | null} */ (ev.target);
|
|
1157
1425
|
while (n && n !== delegateRoot) {
|
|
1158
1426
|
if (n.nodeType === 1) {
|
|
1159
|
-
const
|
|
1427
|
+
const el = /** @type {Element} */ (n);
|
|
1428
|
+
const act = el.__vmzAct || el.getAttribute('data-vmz-act');
|
|
1429
|
+
if (typeof act === 'string' && act) {
|
|
1430
|
+
actionHandler(act).call(inst, ev, el);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const bag = el.__vmzEvt;
|
|
1160
1434
|
if (bag && typeof bag[type] === 'function') {
|
|
1161
|
-
|
|
1435
|
+
// Pass the element so shared each-item handlers can read __vmzBox.
|
|
1436
|
+
bag[type].call(inst, ev, el);
|
|
1162
1437
|
return;
|
|
1163
1438
|
}
|
|
1164
1439
|
}
|
|
@@ -1183,27 +1458,781 @@ const directApi = {
|
|
|
1183
1458
|
if (node.nodeType === 1) {
|
|
1184
1459
|
if (node.__vmzEvt)
|
|
1185
1460
|
node.__vmzEvt = null;
|
|
1461
|
+
if (node.__vmzBox)
|
|
1462
|
+
node.__vmzBox = null;
|
|
1463
|
+
if (node.__vmzAct)
|
|
1464
|
+
node.__vmzAct = null;
|
|
1465
|
+
if (node.__vmzKey != null)
|
|
1466
|
+
node.__vmzKey = null;
|
|
1186
1467
|
for (let c = node.firstChild; c; c = c.nextSibling)
|
|
1187
1468
|
walk(c);
|
|
1188
1469
|
}
|
|
1189
1470
|
};
|
|
1190
1471
|
walk(root);
|
|
1191
1472
|
};
|
|
1473
|
+
const pathFromRoot = (root, node) => {
|
|
1474
|
+
/** @type {number[]} */
|
|
1475
|
+
const path = [];
|
|
1476
|
+
let n = /** @type {Node | null} */ (node);
|
|
1477
|
+
while (n && n !== root) {
|
|
1478
|
+
const parent = n.parentNode;
|
|
1479
|
+
if (!parent)
|
|
1480
|
+
return null;
|
|
1481
|
+
let i = 0;
|
|
1482
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
1483
|
+
if (c === n)
|
|
1484
|
+
break;
|
|
1485
|
+
i++;
|
|
1486
|
+
}
|
|
1487
|
+
path.push(i);
|
|
1488
|
+
n = parent;
|
|
1489
|
+
}
|
|
1490
|
+
if (n !== root)
|
|
1491
|
+
return null;
|
|
1492
|
+
path.reverse();
|
|
1493
|
+
return path;
|
|
1494
|
+
};
|
|
1495
|
+
const nodeAtPath = (root, path) => {
|
|
1496
|
+
let n = /** @type {Node | null} */ (root);
|
|
1497
|
+
for (let i = 0; i < path.length; i++) {
|
|
1498
|
+
if (!n)
|
|
1499
|
+
return null;
|
|
1500
|
+
n = n.childNodes[path[i]] || null;
|
|
1501
|
+
}
|
|
1502
|
+
return n;
|
|
1503
|
+
};
|
|
1504
|
+
/**
|
|
1505
|
+
* Shared each-item event handlers (one per method name for the whole block).
|
|
1506
|
+
* Element carries `__vmzBox`; delegate passes the element as 2nd arg.
|
|
1507
|
+
* @type {Record<string, (ev: Event, el: Element) => void>}
|
|
1508
|
+
*/
|
|
1509
|
+
const sharedActions = Object.create(null);
|
|
1510
|
+
/** @type {Record<string, string>} method → item field for action arg (fallback blueprint). */
|
|
1511
|
+
const actionArgFields = Object.create(null);
|
|
1512
|
+
const actionHandler = (method) => {
|
|
1513
|
+
if (!sharedActions[method]) {
|
|
1514
|
+
sharedActions[method] = function (ev, el) {
|
|
1515
|
+
let n = /** @type {Node | null} */ (el);
|
|
1516
|
+
while (n && n.nodeType === 1) {
|
|
1517
|
+
const box = /** @type {Element} */ (n).__vmzBox;
|
|
1518
|
+
if (box) {
|
|
1519
|
+
const item = box.item != null ? box.item : box;
|
|
1520
|
+
const argField = rowActArgField != null ? rowActArgField : actionArgFields[method] != null ? actionArgFields[method] : null;
|
|
1521
|
+
if (argField == null || item == null)
|
|
1522
|
+
return;
|
|
1523
|
+
const arg = item[argField];
|
|
1524
|
+
const fn = this[method];
|
|
1525
|
+
if (typeof fn === 'function')
|
|
1526
|
+
fn.call(this, arg);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
n = n.parentNode;
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
return sharedActions[method];
|
|
1534
|
+
};
|
|
1535
|
+
/**
|
|
1536
|
+
* @returns {{ method: string, argField: string } | null}
|
|
1537
|
+
*/
|
|
1538
|
+
const parseActionMethod = (handler) => {
|
|
1539
|
+
if (typeof handler !== 'function')
|
|
1540
|
+
return null;
|
|
1541
|
+
try {
|
|
1542
|
+
const src = Function.prototype.toString.call(handler);
|
|
1543
|
+
// this.m(box.item.<field>) — field from author surface.
|
|
1544
|
+
const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(\s*[A-Za-z_$][\w$]*\.item\.([A-Za-z_$][\w$]*)\s*\)/);
|
|
1545
|
+
return m ? { method: m[1], argField: m[2] } : null;
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
/**
|
|
1552
|
+
* Row blueprint: first createItem records dynamic slots; later rows clone + hydrate
|
|
1553
|
+
* without re-running compiled createItem (avoids per-row get/CF/on closures).
|
|
1554
|
+
* @type {null | {
|
|
1555
|
+
* tpl: Element,
|
|
1556
|
+
* texts: Array<{ path: number[], bindingId: any, deps: string[], field: string, get: (root: Element) => Node }>,
|
|
1557
|
+
* attrs: Array<{ path: number[], bindingId: any, deps: string[], name: string, onVal: string, offVal: string, get: (root: Element) => Element }>,
|
|
1558
|
+
* ons: Array<{ path: number[], type: string, method: string, get: (root: Element) => Element }>,
|
|
1559
|
+
* bindIds: Set<string>,
|
|
1560
|
+
* }}
|
|
1561
|
+
*/
|
|
1562
|
+
let blueprint = null;
|
|
1563
|
+
let blueprintOk = true;
|
|
1564
|
+
/** @type {Set<string> | null} */
|
|
1565
|
+
let blueprintBindIds = null;
|
|
1566
|
+
/** @type {null | ((root: Element, entry: any) => void)} */
|
|
1567
|
+
let hydrateBp = null;
|
|
1568
|
+
/** @type {null | ((entry: any) => void)} */
|
|
1569
|
+
let applyBp = null;
|
|
1570
|
+
/**
|
|
1571
|
+
* Compile-time rowKernel installed — static HTML rows, no nested component dispose.
|
|
1572
|
+
* Shape-specific walks live in emitted hydrate/apply (Rust), not here.
|
|
1573
|
+
*/
|
|
1574
|
+
let hasRowKernel = false;
|
|
1575
|
+
/** @type {string | null} item field used as key when rowKernel.keyField is set */
|
|
1576
|
+
let rowKeyField = null;
|
|
1577
|
+
/** @type {string | null} item field passed to delegated actions (from rowKernel.actArgField) */
|
|
1578
|
+
let rowActArgField = null;
|
|
1579
|
+
/** Recycle object bp entries (runtime-recorded blueprint fallback). */
|
|
1580
|
+
/** @type {any[]} */
|
|
1581
|
+
const entryPool = [];
|
|
1582
|
+
const allocBpEntry = () => {
|
|
1583
|
+
const e = entryPool.pop();
|
|
1584
|
+
if (e)
|
|
1585
|
+
return e;
|
|
1586
|
+
return { item: null, dom: null, bp: 1, t0: null, t1: null };
|
|
1587
|
+
};
|
|
1588
|
+
const releaseBpEntry = (entry) => {
|
|
1589
|
+
if (!entry)
|
|
1590
|
+
return;
|
|
1591
|
+
// DOM-as-entry (Element): drop expandos.
|
|
1592
|
+
if (entry.nodeType === 1) {
|
|
1593
|
+
entry.__vmzBox = null;
|
|
1594
|
+
entry.__vmzT0 = null;
|
|
1595
|
+
entry.__vmzT1 = null;
|
|
1596
|
+
entry.__vmzBp = null;
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (!entry.bp || entryPool.length >= 4096)
|
|
1600
|
+
return;
|
|
1601
|
+
entry.item = null;
|
|
1602
|
+
entry.dom = null;
|
|
1603
|
+
entry.t0 = null;
|
|
1604
|
+
entry.t1 = null;
|
|
1605
|
+
entry.a0 = null;
|
|
1606
|
+
entry.patches = null;
|
|
1607
|
+
entryPool.push(entry);
|
|
1608
|
+
};
|
|
1609
|
+
const entryDom = (entry) => (entry && entry.nodeType === 1 ? entry : entry && entry.dom);
|
|
1610
|
+
const entryIsBp = (entry) => !!(entry && (entry.nodeType === 1 || entry.bp || entry.__vmzBp));
|
|
1611
|
+
const entryItem = (entry) => {
|
|
1612
|
+
if (!entry)
|
|
1613
|
+
return null;
|
|
1614
|
+
if (entry.nodeType === 1)
|
|
1615
|
+
return entry.__vmzBox;
|
|
1616
|
+
if (entry.bp)
|
|
1617
|
+
return entry.item;
|
|
1618
|
+
return entry.box && entry.box.item;
|
|
1619
|
+
};
|
|
1620
|
+
const rowKeyOf = (item, index) => {
|
|
1621
|
+
if (rowKeyField != null && item != null)
|
|
1622
|
+
return item[rowKeyField];
|
|
1623
|
+
return keyOf(item, index);
|
|
1624
|
+
};
|
|
1625
|
+
/** Drop all row DOM between markers; rowKernel rows skip per-node dispose walks. */
|
|
1626
|
+
const fastWipeRows = () => {
|
|
1627
|
+
const parent = end.parentNode;
|
|
1628
|
+
if (!parent) {
|
|
1629
|
+
keyed.clear();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
let node = start.nextSibling;
|
|
1633
|
+
if (node && node !== end) {
|
|
1634
|
+
if (hasRowKernel || (blueprint && blueprintOk)) {
|
|
1635
|
+
const range = document.createRange();
|
|
1636
|
+
range.setStartBefore(node);
|
|
1637
|
+
range.setEndBefore(end);
|
|
1638
|
+
range.deleteContents();
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
while (node && node !== end) {
|
|
1642
|
+
const next = node.nextSibling;
|
|
1643
|
+
noteDomRemove();
|
|
1644
|
+
clearDomEvt(node);
|
|
1645
|
+
disposeDomTree(node);
|
|
1646
|
+
node.remove();
|
|
1647
|
+
node = next;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (const [, entry] of keyed)
|
|
1652
|
+
releaseBpEntry(entry);
|
|
1653
|
+
keyed.clear();
|
|
1654
|
+
};
|
|
1655
|
+
const makeChildGetter = (path) => {
|
|
1656
|
+
const len = path.length;
|
|
1657
|
+
if (len === 0)
|
|
1658
|
+
return (root) => root;
|
|
1659
|
+
if (len === 1) {
|
|
1660
|
+
const a = path[0];
|
|
1661
|
+
return (root) => root.childNodes[a];
|
|
1662
|
+
}
|
|
1663
|
+
if (len === 2) {
|
|
1664
|
+
const a = path[0];
|
|
1665
|
+
const b = path[1];
|
|
1666
|
+
return (root) => root.childNodes[a].childNodes[b];
|
|
1667
|
+
}
|
|
1668
|
+
if (len === 3) {
|
|
1669
|
+
const a = path[0];
|
|
1670
|
+
const b = path[1];
|
|
1671
|
+
const c = path[2];
|
|
1672
|
+
return (root) => root.childNodes[a].childNodes[b].childNodes[c];
|
|
1673
|
+
}
|
|
1674
|
+
return (root) => {
|
|
1675
|
+
let n = /** @type {Node} */ (root);
|
|
1676
|
+
for (let i = 0; i < len; i++)
|
|
1677
|
+
n = n.childNodes[path[i]];
|
|
1678
|
+
return n;
|
|
1679
|
+
};
|
|
1680
|
+
};
|
|
1681
|
+
const userCreateItem = spec.createItem;
|
|
1682
|
+
// Compile-time row kernel (Rust Direct emit) — skip runtime blueprint recording.
|
|
1683
|
+
if (spec.rowKernel && typeof spec.rowKernel.html === 'string' && typeof spec.rowKernel.hydrate === 'function') {
|
|
1684
|
+
try {
|
|
1685
|
+
const tplHost = document.createElement('template');
|
|
1686
|
+
tplHost.innerHTML = spec.rowKernel.html;
|
|
1687
|
+
const row = tplHost.content.firstElementChild;
|
|
1688
|
+
if (row && row.nodeType === 1) {
|
|
1689
|
+
blueprint = {
|
|
1690
|
+
tpl: /** @type {Element} */ (row.cloneNode(true)),
|
|
1691
|
+
texts: [],
|
|
1692
|
+
attrs: [],
|
|
1693
|
+
ons: [],
|
|
1694
|
+
bindIds: new Set(),
|
|
1695
|
+
};
|
|
1696
|
+
blueprintOk = true;
|
|
1697
|
+
hasRowKernel = true;
|
|
1698
|
+
rowKeyField = typeof spec.rowKernel.keyField === 'string' && spec.rowKernel.keyField ? spec.rowKernel.keyField : null;
|
|
1699
|
+
rowActArgField =
|
|
1700
|
+
typeof spec.rowKernel.actArgField === 'string' && spec.rowKernel.actArgField ? spec.rowKernel.actArgField : null;
|
|
1701
|
+
blueprintBindIds = new Set(['__vmzRk']);
|
|
1702
|
+
for (const ev of spec.rowKernel.events || [])
|
|
1703
|
+
needDelegate(ev);
|
|
1704
|
+
for (const hf of spec.rowKernel.hostFields || []) {
|
|
1705
|
+
if (typeof hf === 'string' && hf)
|
|
1706
|
+
ensureHostDispatcher(hf);
|
|
1707
|
+
}
|
|
1708
|
+
// Leaf path writes (`rows.0.label`) need `rows.*.label`, not bare `rows.*`.
|
|
1709
|
+
{
|
|
1710
|
+
const listRoot = depRootField((deps && deps[0]) || '') || (deps && deps[0]) || '';
|
|
1711
|
+
if (listRoot) {
|
|
1712
|
+
/** @type {string[]} */
|
|
1713
|
+
const leafDeps = [`${listRoot}.*`];
|
|
1714
|
+
const fields = Array.isArray(spec.rowKernel.itemFields) ? spec.rowKernel.itemFields : [];
|
|
1715
|
+
for (const f of fields) {
|
|
1716
|
+
if (typeof f === 'string' && f)
|
|
1717
|
+
leafDeps.push(`${listRoot}.*.${f}`);
|
|
1718
|
+
}
|
|
1719
|
+
ensureListDispatcher('__vmzRk', leafDeps);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const rkHydrate = spec.rowKernel.hydrate;
|
|
1723
|
+
const rkApply = spec.rowKernel.apply;
|
|
1724
|
+
hydrateBp = (root, entry) => {
|
|
1725
|
+
const item = entry && typeof entry === 'object' && 'item' in entry && entry.item != null
|
|
1726
|
+
? entry.item
|
|
1727
|
+
: entry && entry.__vmzBox != null
|
|
1728
|
+
? entry.__vmzBox
|
|
1729
|
+
: entry;
|
|
1730
|
+
rkHydrate.call(inst, root, item);
|
|
1731
|
+
};
|
|
1732
|
+
applyBp = (entry) => {
|
|
1733
|
+
const root = entry && entry.nodeType === 1 ? entry : entry.dom;
|
|
1734
|
+
const item = entry && entry.nodeType === 1 ? entry.__vmzBox : entry.item;
|
|
1735
|
+
if (typeof rkApply === 'function')
|
|
1736
|
+
rkApply.call(inst, root, item);
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
catch (err) {
|
|
1741
|
+
console.error('vmz:dom rowKernel', err);
|
|
1742
|
+
blueprint = null;
|
|
1743
|
+
blueprintOk = true;
|
|
1744
|
+
hasRowKernel = false;
|
|
1745
|
+
rowKeyField = null;
|
|
1746
|
+
rowActArgField = null;
|
|
1747
|
+
hydrateBp = null;
|
|
1748
|
+
applyBp = null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const probeItemField = (get, box) => {
|
|
1752
|
+
const item = box.item;
|
|
1753
|
+
if (!item || (typeof item !== 'object' && typeof item !== 'function'))
|
|
1754
|
+
return null;
|
|
1755
|
+
let field = null;
|
|
1756
|
+
const proxy = new Proxy(item, {
|
|
1757
|
+
get(t, p, r) {
|
|
1758
|
+
if (typeof p === 'string' || typeof p === 'symbol')
|
|
1759
|
+
field = String(p);
|
|
1760
|
+
return Reflect.get(t, p, r);
|
|
1761
|
+
},
|
|
1762
|
+
});
|
|
1763
|
+
const prev = box.item;
|
|
1764
|
+
box.item = proxy;
|
|
1765
|
+
try {
|
|
1766
|
+
get.call(inst);
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
/* ignore */
|
|
1770
|
+
}
|
|
1771
|
+
box.item = prev;
|
|
1772
|
+
return field;
|
|
1773
|
+
};
|
|
1774
|
+
/**
|
|
1775
|
+
* Probe on/off class strings for `this.<host> === item.<itemField> ? … : …`.
|
|
1776
|
+
* Host/item field names come from binding deps — not hardcoded.
|
|
1777
|
+
*/
|
|
1778
|
+
const probeHostItemClass = (get, box, hostField, itemField) => {
|
|
1779
|
+
if (!hostField || !itemField)
|
|
1780
|
+
return { onVal: '', offVal: '' };
|
|
1781
|
+
const prev = inst[hostField];
|
|
1782
|
+
const matchVal = box.item != null ? box.item[itemField] : undefined;
|
|
1783
|
+
let onVal = '';
|
|
1784
|
+
let offVal = '';
|
|
1785
|
+
const quiet = !!inst.__vmzQuiet;
|
|
1786
|
+
inst.__vmzQuiet = true;
|
|
1787
|
+
try {
|
|
1788
|
+
inst[hostField] = matchVal;
|
|
1789
|
+
onVal = String(get.call(inst) ?? '');
|
|
1790
|
+
// Distinct off value for number / other keys.
|
|
1791
|
+
if (typeof matchVal === 'number') {
|
|
1792
|
+
inst[hostField] = matchVal === 0 ? -1 : 0;
|
|
1793
|
+
if (inst[hostField] === matchVal)
|
|
1794
|
+
inst[hostField] = undefined;
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
inst[hostField] = matchVal === '' ? '__vmz_off__' : '';
|
|
1798
|
+
if (inst[hostField] === matchVal)
|
|
1799
|
+
inst[hostField] = undefined;
|
|
1800
|
+
}
|
|
1801
|
+
offVal = String(get.call(inst) ?? '');
|
|
1802
|
+
}
|
|
1803
|
+
catch {
|
|
1804
|
+
onVal = '';
|
|
1805
|
+
offVal = '';
|
|
1806
|
+
}
|
|
1807
|
+
finally {
|
|
1808
|
+
inst[hostField] = prev;
|
|
1809
|
+
inst.__vmzQuiet = quiet;
|
|
1810
|
+
}
|
|
1811
|
+
return { onVal, offVal };
|
|
1812
|
+
};
|
|
1813
|
+
const sealBlueprintDispatchers = () => {
|
|
1814
|
+
if (!blueprint || blueprintBindIds)
|
|
1815
|
+
return;
|
|
1816
|
+
/** @type {Set<string>} */
|
|
1817
|
+
const ids = new Set();
|
|
1818
|
+
for (const s of blueprint.texts) {
|
|
1819
|
+
if (s.bindingId != null) {
|
|
1820
|
+
ids.add(String(s.bindingId));
|
|
1821
|
+
ensureListDispatcher(s.bindingId, s.deps);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
for (const s of blueprint.attrs) {
|
|
1825
|
+
if (s.bindingId != null) {
|
|
1826
|
+
ids.add(String(s.bindingId));
|
|
1827
|
+
ensureListDispatcher(s.bindingId, s.deps);
|
|
1828
|
+
}
|
|
1829
|
+
for (const d of s.deps || []) {
|
|
1830
|
+
if (!d || d.includes('.*') || (d.includes('[') && d.includes(']')))
|
|
1831
|
+
continue;
|
|
1832
|
+
const rootField = depRootField(d) || d;
|
|
1833
|
+
if (rootField && rootField.indexOf('.') < 0)
|
|
1834
|
+
ensureHostDispatcher(rootField);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
for (const s of blueprint.ons) {
|
|
1838
|
+
needDelegate(s.type);
|
|
1839
|
+
}
|
|
1840
|
+
blueprintBindIds = ids;
|
|
1841
|
+
blueprint.bindIds = ids;
|
|
1842
|
+
compileBlueprintKernels();
|
|
1843
|
+
};
|
|
1844
|
+
const compileBlueprintKernels = () => {
|
|
1845
|
+
if (!blueprint || hydrateBp)
|
|
1846
|
+
return;
|
|
1847
|
+
const textSlots = blueprint.texts;
|
|
1848
|
+
const attrSlots = blueprint.attrs;
|
|
1849
|
+
const onSlots = blueprint.ons;
|
|
1850
|
+
const nText = textSlots.length;
|
|
1851
|
+
const nAttr = attrSlots.length;
|
|
1852
|
+
const nOn = onSlots.length;
|
|
1853
|
+
// Fallback only (no compile-time rowKernel). Field/path walks come from
|
|
1854
|
+
// recorded slots — shape-specific kernels belong in row_kernel.rs.
|
|
1855
|
+
hydrateBp = (root, entry) => {
|
|
1856
|
+
const item = entry && entry.item != null ? entry.item : entry;
|
|
1857
|
+
root.__vmzBox = item;
|
|
1858
|
+
/** @type {Array<Text>} */
|
|
1859
|
+
const textNodes = new Array(nText);
|
|
1860
|
+
/** @type {Array<Element>} */
|
|
1861
|
+
const attrEls = new Array(nAttr);
|
|
1862
|
+
for (let i = 0; i < nText; i++)
|
|
1863
|
+
textNodes[i] = /** @type {Text} */ (textSlots[i].get(root));
|
|
1864
|
+
for (let i = 0; i < nAttr; i++)
|
|
1865
|
+
attrEls[i] = attrSlots[i].get(root);
|
|
1866
|
+
for (let i = 0; i < nOn; i++) {
|
|
1867
|
+
const el = onSlots[i].get(root);
|
|
1868
|
+
if (!el.getAttribute('data-vmz-act')) {
|
|
1869
|
+
el.setAttribute('data-vmz-act', onSlots[i].method);
|
|
1870
|
+
el.__vmzAct = onSlots[i].method;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
for (let i = 0; i < nText; i++) {
|
|
1874
|
+
const v = item == null ? '' : item[textSlots[i].field];
|
|
1875
|
+
textNodes[i].nodeValue = v == null ? '' : v + '';
|
|
1876
|
+
}
|
|
1877
|
+
for (let i = 0; i < nAttr; i++) {
|
|
1878
|
+
const s = attrSlots[i];
|
|
1879
|
+
const el = attrEls[i];
|
|
1880
|
+
const host = s.hostField;
|
|
1881
|
+
const itemKey = s.itemField;
|
|
1882
|
+
if (!host || !itemKey)
|
|
1883
|
+
continue;
|
|
1884
|
+
const hv = inst[host];
|
|
1885
|
+
if (hv != null && item && hv === item[itemKey]) {
|
|
1886
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1887
|
+
el.className = s.onVal;
|
|
1888
|
+
else
|
|
1889
|
+
applyDomAttr(el, s.name, s.onVal);
|
|
1890
|
+
}
|
|
1891
|
+
else if (s.offVal) {
|
|
1892
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1893
|
+
el.className = s.offVal;
|
|
1894
|
+
else
|
|
1895
|
+
applyDomAttr(el, s.name, s.offVal);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
entry.tn = textNodes;
|
|
1899
|
+
entry.ae = attrEls;
|
|
1900
|
+
entry.dom = root;
|
|
1901
|
+
entry.bp = true;
|
|
1902
|
+
};
|
|
1903
|
+
applyBp = (entry) => {
|
|
1904
|
+
const item = entry.item != null ? entry.item : entry.__vmzBox;
|
|
1905
|
+
const textNodes = entry.tn;
|
|
1906
|
+
const attrEls = entry.ae;
|
|
1907
|
+
for (let i = 0; i < nText; i++) {
|
|
1908
|
+
const v = item == null ? '' : item[textSlots[i].field];
|
|
1909
|
+
textNodes[i].nodeValue = v == null ? '' : v + '';
|
|
1910
|
+
}
|
|
1911
|
+
for (let i = 0; i < nAttr; i++) {
|
|
1912
|
+
const s = attrSlots[i];
|
|
1913
|
+
const el = attrEls[i];
|
|
1914
|
+
if (!s.hostField || !s.itemField)
|
|
1915
|
+
continue;
|
|
1916
|
+
const raw = item && inst[s.hostField] === item[s.itemField] ? s.onVal : s.offVal;
|
|
1917
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1918
|
+
el.className = raw;
|
|
1919
|
+
else
|
|
1920
|
+
applyDomAttr(el, s.name, raw);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
};
|
|
1924
|
+
const wireBlueprintItem = (root, box, patches) => {
|
|
1925
|
+
if (!blueprint)
|
|
1926
|
+
return null;
|
|
1927
|
+
sealBlueprintDispatchers();
|
|
1928
|
+
const entry = {
|
|
1929
|
+
item: box.item,
|
|
1930
|
+
index: box.index,
|
|
1931
|
+
dom: root,
|
|
1932
|
+
bp: true,
|
|
1933
|
+
t0: null,
|
|
1934
|
+
t1: null,
|
|
1935
|
+
a0: null,
|
|
1936
|
+
tn: null,
|
|
1937
|
+
ae: null,
|
|
1938
|
+
patches: patches || null,
|
|
1939
|
+
};
|
|
1940
|
+
hydrateBp(root, entry);
|
|
1941
|
+
if (patches) {
|
|
1942
|
+
const applyAll = () => applyBp(entry);
|
|
1943
|
+
applyAll.__vmzBindingIds = blueprintBindIds;
|
|
1944
|
+
applyAll.__vmzBindingId = null;
|
|
1945
|
+
applyAll.__vmzItemLocal = true;
|
|
1946
|
+
applyAll.__vmzBpEntry = entry;
|
|
1947
|
+
patches.push(applyAll);
|
|
1948
|
+
}
|
|
1949
|
+
return entry;
|
|
1950
|
+
};
|
|
1951
|
+
const recordFirstItem = (api, box, patches) => {
|
|
1952
|
+
/** @type {Element | null} */
|
|
1953
|
+
let root = null;
|
|
1954
|
+
/** Pending slots keep live node refs — Direct emit binds before appendChild. */
|
|
1955
|
+
/** @type {{ texts: any[], attrs: any[], ons: any[] }} */
|
|
1956
|
+
const pending = { texts: [], attrs: [], ons: [] };
|
|
1957
|
+
let recordFailed = false;
|
|
1958
|
+
const recordingApi = Object.assign({}, api, {
|
|
1959
|
+
el(tag) {
|
|
1960
|
+
const el = api.el(tag);
|
|
1961
|
+
if (!root)
|
|
1962
|
+
root = el;
|
|
1963
|
+
return el;
|
|
1964
|
+
},
|
|
1965
|
+
// Capture only — do not wireDirectBind (would orphan first-row binders).
|
|
1966
|
+
bindText(i, bindingId, deps, get, textNode, cf) {
|
|
1967
|
+
if (!root)
|
|
1968
|
+
return;
|
|
1969
|
+
// Blueprint recording aborted: fall back to normal wiring for remaining binds.
|
|
1970
|
+
if (recordFailed) {
|
|
1971
|
+
api.bindText(i, bindingId, deps, get, textNode, cf);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
const raw = get.call(inst);
|
|
1976
|
+
if (textNode.nodeType === 3) /** @type {Text} */
|
|
1977
|
+
(textNode).nodeValue = String(raw ?? '');
|
|
1978
|
+
else
|
|
1979
|
+
textNode.textContent = String(raw ?? '');
|
|
1980
|
+
}
|
|
1981
|
+
catch {
|
|
1982
|
+
/* ignore */
|
|
1983
|
+
}
|
|
1984
|
+
pending.texts.push({
|
|
1985
|
+
node: textNode,
|
|
1986
|
+
bindingId,
|
|
1987
|
+
deps: Array.isArray(deps) ? deps.slice() : [],
|
|
1988
|
+
getFn: get,
|
|
1989
|
+
});
|
|
1990
|
+
},
|
|
1991
|
+
bindAttr(i, bindingId, deps, get, el, name, cf) {
|
|
1992
|
+
if (!root)
|
|
1993
|
+
return;
|
|
1994
|
+
if (recordFailed) {
|
|
1995
|
+
api.bindAttr(i, bindingId, deps, get, el, name, cf);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
// Class bind eligible when deps include a bare host field (any name).
|
|
1999
|
+
const hasHostDep = (deps || []).some((d) => d && !d.includes('.*') && !d.includes('[') && String(d).indexOf('.') < 0);
|
|
2000
|
+
if ((name === 'class' || name === 'className') && hasHostDep) {
|
|
2001
|
+
try {
|
|
2002
|
+
const raw = get.call(inst);
|
|
2003
|
+
el.className = raw == null ? '' : String(raw);
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
/* ignore */
|
|
2007
|
+
}
|
|
2008
|
+
pending.attrs.push({
|
|
2009
|
+
node: el,
|
|
2010
|
+
bindingId,
|
|
2011
|
+
deps: Array.isArray(deps) ? deps.slice() : [],
|
|
2012
|
+
name,
|
|
2013
|
+
getFn: get,
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
else {
|
|
2017
|
+
recordFailed = true;
|
|
2018
|
+
api.bindAttr(i, bindingId, deps, get, el, name, cf);
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
on(el, type, handler) {
|
|
2022
|
+
if (recordFailed) {
|
|
2023
|
+
api.on(el, type, handler);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const parsed = parseActionMethod(handler);
|
|
2027
|
+
if (parsed && root) {
|
|
2028
|
+
pending.ons.push({
|
|
2029
|
+
node: el,
|
|
2030
|
+
type,
|
|
2031
|
+
method: parsed.method,
|
|
2032
|
+
argField: parsed.argField,
|
|
2033
|
+
});
|
|
2034
|
+
actionArgFields[parsed.method] = parsed.argField;
|
|
2035
|
+
if (rowActArgField == null)
|
|
2036
|
+
rowActArgField = parsed.argField;
|
|
2037
|
+
root.__vmzBox = box;
|
|
2038
|
+
el.__vmzAct = parsed.method;
|
|
2039
|
+
// Attribute survives cloneNode — hydrate skips per-row __vmzAct writes.
|
|
2040
|
+
el.setAttribute('data-vmz-act', parsed.method);
|
|
2041
|
+
needDelegate(type);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
api.on(el, type, handler);
|
|
2045
|
+
recordFailed = true;
|
|
2046
|
+
},
|
|
2047
|
+
});
|
|
2048
|
+
const dom = userCreateItem.call(inst, recordingApi, box);
|
|
2049
|
+
if (!recordFailed && dom && dom.nodeType === 1 && root === dom && (pending.texts.length > 0 || pending.attrs.length > 0)) {
|
|
2050
|
+
/** @type {any[]} */
|
|
2051
|
+
const texts = [];
|
|
2052
|
+
/** @type {any[]} */
|
|
2053
|
+
const attrs = [];
|
|
2054
|
+
/** @type {any[]} */
|
|
2055
|
+
const ons = [];
|
|
2056
|
+
for (const p of pending.texts) {
|
|
2057
|
+
const path = pathFromRoot(root, p.node);
|
|
2058
|
+
const field = probeItemField(p.getFn, box);
|
|
2059
|
+
if (!path || !field) {
|
|
2060
|
+
recordFailed = true;
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
texts.push({
|
|
2064
|
+
path,
|
|
2065
|
+
bindingId: p.bindingId,
|
|
2066
|
+
deps: p.deps,
|
|
2067
|
+
field,
|
|
2068
|
+
get: makeChildGetter(path),
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
if (!recordFailed) {
|
|
2072
|
+
for (const p of pending.attrs) {
|
|
2073
|
+
const path = pathFromRoot(root, p.node);
|
|
2074
|
+
if (!path) {
|
|
2075
|
+
recordFailed = true;
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
const hostField = (() => {
|
|
2079
|
+
for (const d of p.deps || []) {
|
|
2080
|
+
if (!d)
|
|
2081
|
+
continue;
|
|
2082
|
+
if (d.includes('.*') || d.includes('['))
|
|
2083
|
+
continue;
|
|
2084
|
+
if (String(d).indexOf('.') < 0)
|
|
2085
|
+
return String(d);
|
|
2086
|
+
}
|
|
2087
|
+
return null;
|
|
2088
|
+
})();
|
|
2089
|
+
let itemField = null;
|
|
2090
|
+
for (const d of p.deps || []) {
|
|
2091
|
+
if (!d)
|
|
2092
|
+
continue;
|
|
2093
|
+
if (d.includes('.*')) {
|
|
2094
|
+
const m = String(d).match(/\*\.([A-Za-z_$][\w$]*)$/);
|
|
2095
|
+
if (m)
|
|
2096
|
+
itemField = m[1];
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (!hostField || !itemField) {
|
|
2100
|
+
recordFailed = true;
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
const { onVal, offVal } = probeHostItemClass(p.getFn, box, hostField, itemField);
|
|
2104
|
+
attrs.push({
|
|
2105
|
+
path,
|
|
2106
|
+
bindingId: p.bindingId,
|
|
2107
|
+
deps: p.deps,
|
|
2108
|
+
name: p.name,
|
|
2109
|
+
onVal,
|
|
2110
|
+
offVal,
|
|
2111
|
+
hostField,
|
|
2112
|
+
itemField,
|
|
2113
|
+
get: makeChildGetter(path),
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!recordFailed) {
|
|
2118
|
+
for (const p of pending.ons) {
|
|
2119
|
+
const path = pathFromRoot(root, p.node);
|
|
2120
|
+
if (!path) {
|
|
2121
|
+
recordFailed = true;
|
|
2122
|
+
break;
|
|
2123
|
+
}
|
|
2124
|
+
ons.push({
|
|
2125
|
+
path,
|
|
2126
|
+
type: p.type,
|
|
2127
|
+
method: p.method,
|
|
2128
|
+
get: makeChildGetter(path),
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (!recordFailed) {
|
|
2133
|
+
const tpl = /** @type {Element} */ (dom.cloneNode(true));
|
|
2134
|
+
clearDomEvt(tpl);
|
|
2135
|
+
blueprint = {
|
|
2136
|
+
tpl,
|
|
2137
|
+
texts,
|
|
2138
|
+
attrs,
|
|
2139
|
+
ons,
|
|
2140
|
+
bindIds: new Set(),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (!blueprint)
|
|
2145
|
+
blueprintOk = false;
|
|
2146
|
+
return dom;
|
|
2147
|
+
};
|
|
2148
|
+
const createItem = (api, box, patches) => {
|
|
2149
|
+
if (blueprint && blueprintOk) {
|
|
2150
|
+
const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
|
|
2151
|
+
wireBlueprintItem(root, box, patches);
|
|
2152
|
+
return root;
|
|
2153
|
+
}
|
|
2154
|
+
if (blueprintOk && typeof userCreateItem === 'function') {
|
|
2155
|
+
const dom = recordFirstItem(api, box, patches);
|
|
2156
|
+
if (blueprint && dom && dom.nodeType === 1) {
|
|
2157
|
+
patches.length = 0;
|
|
2158
|
+
wireBlueprintItem(dom, box, patches);
|
|
2159
|
+
}
|
|
2160
|
+
return dom;
|
|
2161
|
+
}
|
|
2162
|
+
return userCreateItem.call(inst, api, box);
|
|
2163
|
+
};
|
|
2164
|
+
/**
|
|
2165
|
+
* Reorder / place item DOM with minimal mutations.
|
|
2166
|
+
* - already-correct → no-op
|
|
2167
|
+
* - pure 2-node swap → 1–2 insertBefore (common keyed list swap)
|
|
2168
|
+
* - append prefix → Fragment insert only new tail
|
|
2169
|
+
* - create / replace / complex → Fragment rebuild before `end`
|
|
2170
|
+
*/
|
|
1192
2171
|
const reconcileDomOrder = (nextNodes) => {
|
|
1193
2172
|
const parent = end.parentNode;
|
|
1194
2173
|
if (!parent)
|
|
1195
2174
|
return;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
for (let
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
2175
|
+
/** @type {ChildNode[]} */
|
|
2176
|
+
const curr = [];
|
|
2177
|
+
for (let n = start.nextSibling; n && n !== end; n = n.nextSibling) {
|
|
2178
|
+
curr.push(n);
|
|
2179
|
+
}
|
|
2180
|
+
if (curr.length === nextNodes.length) {
|
|
2181
|
+
let same = true;
|
|
2182
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2183
|
+
if (curr[i] !== nextNodes[i]) {
|
|
2184
|
+
same = false;
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (same)
|
|
2189
|
+
return;
|
|
2190
|
+
// Fast path: exactly two positions swapped (benchmark swaprows).
|
|
2191
|
+
/** @type {number[]} */
|
|
2192
|
+
const diff = [];
|
|
2193
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2194
|
+
if (curr[i] !== nextNodes[i])
|
|
2195
|
+
diff.push(i);
|
|
2196
|
+
}
|
|
2197
|
+
if (diff.length === 2 && curr[diff[0]] === nextNodes[diff[1]] && curr[diff[1]] === nextNodes[diff[0]]) {
|
|
2198
|
+
const a = curr[diff[0]];
|
|
2199
|
+
const b = curr[diff[1]];
|
|
2200
|
+
const aNext = a.nextSibling;
|
|
2201
|
+
const bNext = b.nextSibling;
|
|
2202
|
+
noteDomMove();
|
|
2203
|
+
noteDomMove();
|
|
2204
|
+
if (aNext === b) {
|
|
2205
|
+
parent.insertBefore(b, a);
|
|
2206
|
+
}
|
|
2207
|
+
else if (bNext === a) {
|
|
2208
|
+
parent.insertBefore(a, b);
|
|
2209
|
+
}
|
|
2210
|
+
else {
|
|
2211
|
+
parent.insertBefore(b, aNext);
|
|
2212
|
+
parent.insertBefore(a, bNext);
|
|
2213
|
+
}
|
|
2214
|
+
return;
|
|
1202
2215
|
}
|
|
1203
|
-
n = n.nextSibling;
|
|
1204
2216
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
2217
|
+
// Append-only: existing live prefix unchanged, only new tail detached.
|
|
2218
|
+
if (curr.length < nextNodes.length && curr.length > 0) {
|
|
2219
|
+
let prefix = true;
|
|
2220
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2221
|
+
if (curr[i] !== nextNodes[i]) {
|
|
2222
|
+
prefix = false;
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (prefix) {
|
|
2227
|
+
const batch = document.createDocumentFragment();
|
|
2228
|
+
for (let i = curr.length; i < nextNodes.length; i++) {
|
|
2229
|
+
batch.appendChild(nextNodes[i]);
|
|
2230
|
+
}
|
|
2231
|
+
parent.insertBefore(batch, end);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
// Create / replace / complex reorder: one Fragment write.
|
|
1207
2236
|
const batch = document.createDocumentFragment();
|
|
1208
2237
|
for (const dom of nextNodes) {
|
|
1209
2238
|
if (dom.parentNode)
|
|
@@ -1217,26 +2246,40 @@ const directApi = {
|
|
|
1217
2246
|
return;
|
|
1218
2247
|
const applied = ++gen;
|
|
1219
2248
|
const list = readList();
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
2249
|
+
const n = list.length;
|
|
2250
|
+
// Clear all rows.
|
|
2251
|
+
if (n === 0) {
|
|
2252
|
+
if (keyed.size)
|
|
2253
|
+
fastWipeRows();
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
// Full replace (no key reuse): wipe then fall into fresh create.
|
|
2257
|
+
if (keyed.size > 0 && hasRowKernel) {
|
|
2258
|
+
let reuse = false;
|
|
2259
|
+
for (let i = 0; i < n; i++) {
|
|
2260
|
+
if (keyed.has(rowKeyOf(list[i], i))) {
|
|
2261
|
+
reuse = true;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
1228
2264
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
2265
|
+
if (!reuse)
|
|
2266
|
+
fastWipeRows();
|
|
2267
|
+
}
|
|
2268
|
+
// Fresh create into empty each: record blueprint once, then clone-only.
|
|
2269
|
+
if (keyed.size === 0 && n > 0) {
|
|
2270
|
+
const parent = end.parentNode;
|
|
2271
|
+
const batch = document.createDocumentFragment();
|
|
2272
|
+
let startIdx = 0;
|
|
2273
|
+
if (!blueprint || !blueprintOk) {
|
|
2274
|
+
const box0 = { item: list[0], index: 0 };
|
|
2275
|
+
const patches0 = [];
|
|
1233
2276
|
const prevPatches = directApi._itemPatches;
|
|
1234
2277
|
const prevCtx = directApi._eachCtx;
|
|
1235
|
-
directApi._itemPatches =
|
|
2278
|
+
directApi._itemPatches = patches0;
|
|
1236
2279
|
directApi._eachCtx = eachCtx;
|
|
1237
|
-
let
|
|
2280
|
+
let dom0 = null;
|
|
1238
2281
|
try {
|
|
1239
|
-
|
|
2282
|
+
dom0 = createItem(directApi, box0, patches0);
|
|
1240
2283
|
}
|
|
1241
2284
|
finally {
|
|
1242
2285
|
directApi._itemPatches = prevPatches;
|
|
@@ -1244,24 +2287,235 @@ const directApi = {
|
|
|
1244
2287
|
}
|
|
1245
2288
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
1246
2289
|
return;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
2290
|
+
if (!dom0)
|
|
2291
|
+
return;
|
|
2292
|
+
const k0 = itemKey(box0);
|
|
2293
|
+
if (dom0.nodeType === 1) /** @type {Element} */
|
|
2294
|
+
(dom0).__vmzKey = k0;
|
|
2295
|
+
// First row may already be blueprint-wired (patches cleared + hydrate).
|
|
2296
|
+
let entry0 = keyed.get(k0);
|
|
2297
|
+
if (!entry0) {
|
|
2298
|
+
if (patches0.length && patches0[0] && patches0[0].__vmzBpEntry) {
|
|
2299
|
+
entry0 = patches0[0].__vmzBpEntry;
|
|
2300
|
+
entry0.patches = patches0;
|
|
2301
|
+
}
|
|
2302
|
+
else if (blueprint && blueprintOk) {
|
|
2303
|
+
entry0 = wireBlueprintItem(/** @type {Element} */ (dom0), box0, patches0);
|
|
2304
|
+
}
|
|
2305
|
+
else {
|
|
2306
|
+
tagItemPatches(patches0, 0);
|
|
2307
|
+
entry0 = { box: box0, dom: dom0, patches: patches0 };
|
|
2308
|
+
}
|
|
2309
|
+
keyed.set(k0, entry0);
|
|
2310
|
+
}
|
|
2311
|
+
batch.appendChild(dom0);
|
|
2312
|
+
startIdx = 1;
|
|
2313
|
+
}
|
|
2314
|
+
if (blueprint && blueprintOk) {
|
|
2315
|
+
sealBlueprintDispatchers();
|
|
2316
|
+
const tpl = blueprint.tpl;
|
|
2317
|
+
if (hasRowKernel && spec.rowKernel && typeof spec.rowKernel.create === 'function') {
|
|
2318
|
+
// Shape-specific create loop is Rust-emitted (rowKernel.create).
|
|
2319
|
+
// Direct parent.insertBefore (no Fragment). When parent has only the
|
|
2320
|
+
// each markers as children, detach parent for the fill then reattach —
|
|
2321
|
+
// same structural trick as hand-tuned keyed apps (not app-specific).
|
|
2322
|
+
if (parent) {
|
|
2323
|
+
let detached = null;
|
|
2324
|
+
let reinsertAt = null;
|
|
2325
|
+
if (parent.nodeType === 1 && parent.parentNode) {
|
|
2326
|
+
let onlyMarkers = true;
|
|
2327
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
2328
|
+
if (c !== start && c !== end) {
|
|
2329
|
+
onlyMarkers = false;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (onlyMarkers) {
|
|
2334
|
+
detached = parent.parentNode;
|
|
2335
|
+
reinsertAt = parent.nextSibling;
|
|
2336
|
+
detached.removeChild(parent);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
spec.rowKernel.create.call(inst, list, startIdx, tpl, keyed, parent, end, rowKeyOf);
|
|
2340
|
+
if (detached)
|
|
2341
|
+
detached.insertBefore(parent, reinsertAt);
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
const hydrate = spec.rowKernel.hydrate;
|
|
2345
|
+
for (let i = startIdx; i < n; i++) {
|
|
2346
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2347
|
+
return;
|
|
2348
|
+
const item = list[i];
|
|
2349
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2350
|
+
hydrate.call(inst, root, item);
|
|
2351
|
+
const k = rowKeyOf(item, i);
|
|
2352
|
+
root.__vmzKey = k;
|
|
2353
|
+
keyed.set(k, root);
|
|
2354
|
+
batch.appendChild(root);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
else if (hasRowKernel && hydrateBp) {
|
|
2359
|
+
const hydrate = spec.rowKernel.hydrate;
|
|
2360
|
+
for (let i = startIdx; i < n; i++) {
|
|
2361
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2362
|
+
return;
|
|
2363
|
+
const item = list[i];
|
|
2364
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2365
|
+
hydrate.call(inst, root, item);
|
|
2366
|
+
const k = rowKeyOf(item, i);
|
|
2367
|
+
root.__vmzKey = k;
|
|
2368
|
+
keyed.set(k, root);
|
|
2369
|
+
batch.appendChild(root);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
else {
|
|
2373
|
+
for (let i = startIdx; i < n; i++) {
|
|
2374
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2375
|
+
return;
|
|
2376
|
+
const item = list[i];
|
|
2377
|
+
const k = keyOf(item, i);
|
|
2378
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2379
|
+
const entry = {
|
|
2380
|
+
item,
|
|
2381
|
+
index: i,
|
|
2382
|
+
dom: root,
|
|
2383
|
+
bp: true,
|
|
2384
|
+
t0: null,
|
|
2385
|
+
t1: null,
|
|
2386
|
+
a0: null,
|
|
2387
|
+
patches: null,
|
|
2388
|
+
};
|
|
2389
|
+
hydrateBp(root, entry);
|
|
2390
|
+
root.__vmzKey = k;
|
|
2391
|
+
keyed.set(k, entry);
|
|
2392
|
+
batch.appendChild(root);
|
|
1251
2393
|
}
|
|
1252
|
-
entry = { box, dom, patches };
|
|
1253
|
-
keyed.set(k, entry);
|
|
1254
2394
|
}
|
|
1255
2395
|
}
|
|
1256
2396
|
else {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2397
|
+
for (let i = startIdx; i < n; i++) {
|
|
2398
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2399
|
+
return;
|
|
2400
|
+
const box = { item: list[i], index: i };
|
|
2401
|
+
const k = itemKey(box);
|
|
2402
|
+
const patches = [];
|
|
2403
|
+
const prevPatches = directApi._itemPatches;
|
|
2404
|
+
const prevCtx = directApi._eachCtx;
|
|
2405
|
+
directApi._itemPatches = patches;
|
|
2406
|
+
directApi._eachCtx = eachCtx;
|
|
2407
|
+
let dom = null;
|
|
2408
|
+
try {
|
|
2409
|
+
dom = createItem(directApi, box, patches);
|
|
2410
|
+
}
|
|
2411
|
+
finally {
|
|
2412
|
+
directApi._itemPatches = prevPatches;
|
|
2413
|
+
directApi._eachCtx = prevCtx;
|
|
2414
|
+
}
|
|
2415
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2416
|
+
return;
|
|
2417
|
+
if (!dom)
|
|
2418
|
+
continue;
|
|
2419
|
+
tagItemPatches(patches, i);
|
|
2420
|
+
if (dom.nodeType === 1) /** @type {Element} */
|
|
2421
|
+
(dom).__vmzKey = k;
|
|
2422
|
+
keyed.set(k, { box, dom, patches });
|
|
2423
|
+
batch.appendChild(dom);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2427
|
+
return;
|
|
2428
|
+
if (parent && batch.firstChild)
|
|
2429
|
+
parent.insertBefore(batch, end);
|
|
2430
|
+
if (end.isConnected)
|
|
2431
|
+
ensureDelegateAttached();
|
|
2432
|
+
else
|
|
2433
|
+
queueMicrotask(() => {
|
|
2434
|
+
if (!inst.__vmzDestroyed)
|
|
2435
|
+
ensureDelegateAttached();
|
|
2436
|
+
});
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
const seen = new Set();
|
|
2440
|
+
/** @type {Node[]} */
|
|
2441
|
+
const nextNodes = [];
|
|
2442
|
+
for (let i = 0; i < n; i++) {
|
|
2443
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2444
|
+
return;
|
|
2445
|
+
const item = list[i];
|
|
2446
|
+
const k = rowKeyOf(item, i);
|
|
2447
|
+
seen.add(k);
|
|
2448
|
+
let entry = keyed.get(k);
|
|
2449
|
+
if (!entry) {
|
|
2450
|
+
if (hasRowKernel && blueprint && blueprintOk && hydrateBp) {
|
|
2451
|
+
const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
|
|
2452
|
+
spec.rowKernel.hydrate.call(inst, root, item);
|
|
2453
|
+
root.__vmzKey = k;
|
|
2454
|
+
keyed.set(k, root);
|
|
2455
|
+
entry = root;
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
const box = { item, index: i };
|
|
2459
|
+
const patches = [];
|
|
2460
|
+
const prevPatches = directApi._itemPatches;
|
|
2461
|
+
const prevCtx = directApi._eachCtx;
|
|
2462
|
+
directApi._itemPatches = patches;
|
|
2463
|
+
directApi._eachCtx = eachCtx;
|
|
2464
|
+
let dom = null;
|
|
2465
|
+
try {
|
|
2466
|
+
dom = createItem(directApi, box, patches);
|
|
2467
|
+
}
|
|
2468
|
+
finally {
|
|
2469
|
+
directApi._itemPatches = prevPatches;
|
|
2470
|
+
directApi._eachCtx = prevCtx;
|
|
2471
|
+
}
|
|
2472
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2473
|
+
return;
|
|
2474
|
+
tagItemPatches(patches, i);
|
|
2475
|
+
if (dom) {
|
|
2476
|
+
if (dom.nodeType === 1) {
|
|
2477
|
+
// Client identity: expando only (see 01 each identity). SSR uses data-vmz-key.
|
|
2478
|
+
/** @type {Element} */ (dom).__vmzKey = k;
|
|
2479
|
+
}
|
|
2480
|
+
entry = { box, dom, patches };
|
|
2481
|
+
keyed.set(k, entry);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
else {
|
|
2486
|
+
const sameItem = entryItem(entry) === item;
|
|
2487
|
+
if (entry.nodeType === 1) {
|
|
2488
|
+
entry.__vmzBox = item;
|
|
2489
|
+
}
|
|
2490
|
+
else if (entry.bp) {
|
|
2491
|
+
entry.item = item;
|
|
2492
|
+
entry.index = i;
|
|
2493
|
+
if (entry.dom)
|
|
2494
|
+
entry.dom.__vmzBox = entry.item;
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
entry.box.item = item;
|
|
2498
|
+
entry.box.index = i;
|
|
2499
|
+
tagItemPatches(entry.patches, i);
|
|
2500
|
+
}
|
|
2501
|
+
// Pure reorder (swap / move) keeps object identity — skip leaf patches.
|
|
2502
|
+
if (!sameItem) {
|
|
2503
|
+
if (entryIsBp(entry) && applyBp) {
|
|
2504
|
+
try {
|
|
2505
|
+
applyBp(entry);
|
|
2506
|
+
}
|
|
2507
|
+
catch (err) {
|
|
2508
|
+
console.error('vmz:dom each item', err);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
else if (entry.patches) {
|
|
2512
|
+
for (const p of entry.patches)
|
|
2513
|
+
runPatch(p, null);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
1262
2516
|
}
|
|
1263
2517
|
if (entry)
|
|
1264
|
-
nextNodes.push(entry
|
|
2518
|
+
nextNodes.push(entryDom(entry));
|
|
1265
2519
|
}
|
|
1266
2520
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
1267
2521
|
return;
|
|
@@ -1269,11 +2523,19 @@ const directApi = {
|
|
|
1269
2523
|
if (seen.has(k))
|
|
1270
2524
|
continue;
|
|
1271
2525
|
noteDomRemove();
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
2526
|
+
const dom = entryDom(entry);
|
|
2527
|
+
if (hasRowKernel) {
|
|
2528
|
+
if (dom && dom.parentNode)
|
|
2529
|
+
dom.remove();
|
|
2530
|
+
}
|
|
2531
|
+
else {
|
|
2532
|
+
clearDomEvt(dom);
|
|
2533
|
+
disposeDomTree(dom);
|
|
2534
|
+
if (dom && dom.parentNode)
|
|
2535
|
+
dom.remove();
|
|
2536
|
+
}
|
|
1276
2537
|
keyed.delete(k);
|
|
2538
|
+
releaseBpEntry(entry);
|
|
1277
2539
|
}
|
|
1278
2540
|
reconcileDomOrder(nextNodes);
|
|
1279
2541
|
// First apply may run while start/end still sit in a DocumentFragment
|
|
@@ -1291,27 +2553,41 @@ const directApi = {
|
|
|
1291
2553
|
directApi._itemPatches.push(apply);
|
|
1292
2554
|
start.__vmzDispose = () => {
|
|
1293
2555
|
teardownDelegate();
|
|
1294
|
-
|
|
1295
|
-
clearDomEvt(entry.dom);
|
|
1296
|
-
disposeDomTree(entry.dom);
|
|
1297
|
-
if (entry.dom && entry.dom.parentNode)
|
|
1298
|
-
entry.dom.remove();
|
|
1299
|
-
}
|
|
1300
|
-
keyed.clear();
|
|
2556
|
+
fastWipeRows();
|
|
1301
2557
|
};
|
|
1302
2558
|
const softDeps = [...new Set((deps || []).map((d) => `${depRootField(d)}.*`))];
|
|
1303
2559
|
const softRefresh = () => {
|
|
1304
2560
|
if (inst.__vmzDestroyed)
|
|
1305
2561
|
return;
|
|
2562
|
+
const trie = inst.__vmzFlushTrie;
|
|
2563
|
+
const listRoot = depRootField((deps && deps[0]) || '') || '';
|
|
2564
|
+
// Full list replace is owned by apply(); soft channel is item/structure churn.
|
|
2565
|
+
if (trie && listRoot && trie[listRoot] && trie[listRoot].replace)
|
|
2566
|
+
return;
|
|
1306
2567
|
const list = readList();
|
|
1307
|
-
const softKey = softDeps[0] || `${
|
|
2568
|
+
const softKey = softDeps[0] || `${listRoot}.*`;
|
|
1308
2569
|
for (let i = 0; i < list.length; i++) {
|
|
1309
|
-
const
|
|
1310
|
-
const k =
|
|
2570
|
+
const item = list[i];
|
|
2571
|
+
const k = rowKeyOf(item, i);
|
|
1311
2572
|
const entry = keyed.get(k);
|
|
1312
2573
|
if (!entry)
|
|
1313
2574
|
continue;
|
|
1314
|
-
entry
|
|
2575
|
+
if (entryIsBp(entry)) {
|
|
2576
|
+
if (entry.nodeType === 1)
|
|
2577
|
+
entry.__vmzBox = item;
|
|
2578
|
+
else {
|
|
2579
|
+
entry.item = item;
|
|
2580
|
+
entry.index = i;
|
|
2581
|
+
if (entry.dom)
|
|
2582
|
+
entry.dom.__vmzBox = item;
|
|
2583
|
+
}
|
|
2584
|
+
if (applyBp)
|
|
2585
|
+
applyBp(entry);
|
|
2586
|
+
else if (hydrateBp)
|
|
2587
|
+
hydrateBp(entryDom(entry), entry);
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
entry.box.item = item;
|
|
1315
2591
|
entry.box.index = i;
|
|
1316
2592
|
tagItemPatches(entry.patches, i);
|
|
1317
2593
|
for (const p of entry.patches) {
|
|
@@ -1337,7 +2613,7 @@ const directApi = {
|
|
|
1337
2613
|
/**
|
|
1338
2614
|
* @param {object} inst
|
|
1339
2615
|
* @param {string[]} deps
|
|
1340
|
-
* @param { => any} fn
|
|
2616
|
+
* @param {() => any} fn
|
|
1341
2617
|
* @param {number|string|null|undefined} bindingId
|
|
1342
2618
|
*/
|
|
1343
2619
|
function trackDirectBind(inst, deps, fn, bindingId = null) {
|
|
@@ -1362,14 +2638,14 @@ function trackDirectBind(inst, deps, fn, bindingId = null) {
|
|
|
1362
2638
|
* @param {object} inst
|
|
1363
2639
|
* @param {number|string|null} bindingId
|
|
1364
2640
|
* @param {string[]} deps
|
|
1365
|
-
* @param { => any} get
|
|
2641
|
+
* @param {() => any} get
|
|
1366
2642
|
* @param {(raw: any) => void} write
|
|
1367
2643
|
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
1368
2644
|
*/
|
|
1369
2645
|
function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
1370
2646
|
let activeBranch = -1;
|
|
1371
2647
|
/** @type {string[]} */
|
|
1372
|
-
let liveDeps = Array.isArray(deps) ?
|
|
2648
|
+
let liveDeps = Array.isArray(deps) ? deps.slice() : [];
|
|
1373
2649
|
const pickCf = () => {
|
|
1374
2650
|
if (!cf || !Array.isArray(cf.branches))
|
|
1375
2651
|
return -1;
|
|
@@ -1387,6 +2663,11 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1387
2663
|
}
|
|
1388
2664
|
return cf.branches.length - 1;
|
|
1389
2665
|
};
|
|
2666
|
+
// Item-local CF whose branches only gate the same stable deps: skip branch switching.
|
|
2667
|
+
let simpleCf = false;
|
|
2668
|
+
if (cf && Array.isArray(cf.branches) && directApi._itemPatches) {
|
|
2669
|
+
simpleCf = cf.branches.every((b) => !b.deps || b.deps.length === 0);
|
|
2670
|
+
}
|
|
1390
2671
|
const apply = () => {
|
|
1391
2672
|
if (precision.enabled) {
|
|
1392
2673
|
precision.bindingEvals++;
|
|
@@ -1404,7 +2685,7 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1404
2685
|
raw = null;
|
|
1405
2686
|
}
|
|
1406
2687
|
write(raw);
|
|
1407
|
-
if (!cf || !Array.isArray(cf.branches))
|
|
2688
|
+
if (!cf || !Array.isArray(cf.branches) || simpleCf || apply.__vmzItemLocal)
|
|
1408
2689
|
return;
|
|
1409
2690
|
const next = pickCf();
|
|
1410
2691
|
if (next === activeBranch)
|
|
@@ -1413,21 +2694,19 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1413
2694
|
const branch = cf.branches[next];
|
|
1414
2695
|
const nextDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
|
|
1415
2696
|
const uniq = [...new Set(nextDeps)];
|
|
1416
|
-
// Item-local binds must never enter the global binder table ( / jfb select).
|
|
1417
|
-
if (apply.__vmzItemLocal) {
|
|
1418
|
-
liveDeps = uniq;
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
2697
|
unregisterBind(inst, liveDeps, apply, bindingId);
|
|
1422
2698
|
liveDeps = uniq;
|
|
1423
2699
|
registerBind(inst, liveDeps, apply, bindingId);
|
|
1424
2700
|
};
|
|
1425
|
-
if (cf && Array.isArray(cf.branches)) {
|
|
2701
|
+
if (cf && Array.isArray(cf.branches) && !simpleCf) {
|
|
1426
2702
|
activeBranch = pickCf();
|
|
1427
2703
|
const branch = cf.branches[activeBranch];
|
|
1428
2704
|
liveDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
|
|
1429
2705
|
liveDeps = [...new Set(liveDeps)];
|
|
1430
2706
|
}
|
|
2707
|
+
else if (cf && Array.isArray(cf.branches) && simpleCf) {
|
|
2708
|
+
liveDeps = Array.isArray(cf.stable) && cf.stable.length ? cf.stable : liveDeps;
|
|
2709
|
+
}
|
|
1431
2710
|
// Mark before first apply so CF branch switches never hit global registerBind.
|
|
1432
2711
|
if (directApi._itemPatches)
|
|
1433
2712
|
apply.__vmzItemLocal = true;
|
|
@@ -1437,6 +2716,74 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1437
2716
|
function isEventPropName(name) {
|
|
1438
2717
|
return typeof name === 'string' && /^on[A-Z]/.test(name);
|
|
1439
2718
|
}
|
|
2719
|
+
/** Monotonic id for `bindComponentProp` BindingIds (per process). */
|
|
2720
|
+
let directPropBindSeq = 0;
|
|
2721
|
+
/** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
|
|
2722
|
+
const BOOLEAN_HTML_ATTRS = new Set([
|
|
2723
|
+
'disabled',
|
|
2724
|
+
'checked',
|
|
2725
|
+
'selected',
|
|
2726
|
+
'readonly',
|
|
2727
|
+
'required',
|
|
2728
|
+
'multiple',
|
|
2729
|
+
'hidden',
|
|
2730
|
+
'autofocus',
|
|
2731
|
+
'autoplay',
|
|
2732
|
+
'controls',
|
|
2733
|
+
'loop',
|
|
2734
|
+
'muted',
|
|
2735
|
+
'open',
|
|
2736
|
+
'novalidate',
|
|
2737
|
+
'formnovalidate',
|
|
2738
|
+
'defer',
|
|
2739
|
+
'async',
|
|
2740
|
+
'ismap',
|
|
2741
|
+
'default',
|
|
2742
|
+
'inert',
|
|
2743
|
+
]);
|
|
2744
|
+
/**
|
|
2745
|
+
* @param {Element} el
|
|
2746
|
+
* @param {string} name
|
|
2747
|
+
* @param {any} value
|
|
2748
|
+
*/
|
|
2749
|
+
function applyDomAttr(el, name, value) {
|
|
2750
|
+
const key = name === 'className' ? 'class' : name;
|
|
2751
|
+
if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
|
|
2752
|
+
if (value === false || value == null || value === '') {
|
|
2753
|
+
el.removeAttribute(key);
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
2757
|
+
}
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
if (value == null || value === false)
|
|
2761
|
+
el.removeAttribute(key);
|
|
2762
|
+
else
|
|
2763
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Serialize-tree attr write (SSR).
|
|
2767
|
+
* @param {any} el
|
|
2768
|
+
* @param {string} name
|
|
2769
|
+
* @param {any} value
|
|
2770
|
+
*/
|
|
2771
|
+
function applySerializeAttr(el, name, value) {
|
|
2772
|
+
if (!el || el.__kind !== 'el')
|
|
2773
|
+
return;
|
|
2774
|
+
const key = name === 'className' ? 'class' : name;
|
|
2775
|
+
if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
|
|
2776
|
+
if (value === false || value == null || value === '')
|
|
2777
|
+
delete el.attrs[key];
|
|
2778
|
+
else
|
|
2779
|
+
el.attrs[key] = value === true ? '' : String(value);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
if (value == null || value === false)
|
|
2783
|
+
delete el.attrs[key];
|
|
2784
|
+
else
|
|
2785
|
+
el.attrs[key] = value === true ? '' : String(value);
|
|
2786
|
+
}
|
|
1440
2787
|
function stripFns(obj) {
|
|
1441
2788
|
/** @type {Record<string, unknown>} */
|
|
1442
2789
|
const out = {};
|
|
@@ -1663,18 +3010,15 @@ function runDirectResume(Component, inst, container) {
|
|
|
1663
3010
|
return document.createTextNode(String(s ?? ''));
|
|
1664
3011
|
},
|
|
1665
3012
|
attr(el, name, value) {
|
|
1666
|
-
|
|
1667
|
-
el.setAttribute('class', String(value ?? ''));
|
|
1668
|
-
else if (value == null || value === false)
|
|
1669
|
-
el.removeAttribute(name);
|
|
1670
|
-
else
|
|
1671
|
-
el.setAttribute(name, value === true ? '' : String(value));
|
|
3013
|
+
applyDomAttr(el, name, value);
|
|
1672
3014
|
},
|
|
1673
3015
|
on(el, type, handler) {
|
|
1674
3016
|
el.addEventListener(type, handler);
|
|
1675
3017
|
},
|
|
1676
3018
|
bindText: directApi.bindText,
|
|
1677
3019
|
bindAttr: directApi.bindAttr,
|
|
3020
|
+
bindComponentProp: directApi.bindComponentProp,
|
|
3021
|
+
projectDefaultSlot: directApi.projectDefaultSlot,
|
|
1678
3022
|
setHtml: directApi.setHtml,
|
|
1679
3023
|
bindHtml: directApi.bindHtml,
|
|
1680
3024
|
ifBlock: directApi.ifBlock,
|
|
@@ -1838,7 +3182,7 @@ function eventEntryType(strategy) {
|
|
|
1838
3182
|
}
|
|
1839
3183
|
function scheduleClientOn(el, strategy, fn) {
|
|
1840
3184
|
const run = () => {
|
|
1841
|
-
Promise.resolve(fn).catch((err) => console.error('vmz:dom island', err));
|
|
3185
|
+
Promise.resolve(fn()).catch((err) => console.error('vmz:dom island', err));
|
|
1842
3186
|
};
|
|
1843
3187
|
if (isEventEntryStrategy(strategy)) {
|
|
1844
3188
|
if (!el || typeof el.addEventListener !== 'function') {
|
|
@@ -2325,8 +3669,9 @@ function notifyOwners(owners, localSegs) {
|
|
|
2325
3669
|
}
|
|
2326
3670
|
/**
|
|
2327
3671
|
* Field-owned write traps for plain objects / arrays on state fields.
|
|
2328
|
-
* Plain objects: WriteBarrier via defineProperty (no Proxy)
|
|
2329
|
-
* Arrays: transitional Proxy
|
|
3672
|
+
* Plain objects: WriteBarrier via defineProperty (no Proxy).
|
|
3673
|
+
* Arrays: transitional Proxy tracks list identity/mutators only; elements stay plain
|
|
3674
|
+
* (no per-item wrap on large assign — nested notifies via `__vmzWritePath`).
|
|
2330
3675
|
* Shared raw objects notify **all** current owners.
|
|
2331
3676
|
*
|
|
2332
3677
|
* @param {any} value
|
|
@@ -2408,6 +3753,12 @@ function installOwnedProp(obj, prop, entry) {
|
|
|
2408
3753
|
},
|
|
2409
3754
|
});
|
|
2410
3755
|
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Transitional array Proxy: track list identity / mutators only.
|
|
3758
|
+
* Elements stay plain — no per-item defineProperty on `this.rows = largeArray`
|
|
3759
|
+
* (design: WriteBarrier / list replace must not wrap 1k items). Nested field
|
|
3760
|
+
* notifies go through `__vmzWritePath` or whole-array replace.
|
|
3761
|
+
*/
|
|
2411
3762
|
function wrapArray(arr, report, pathSegs) {
|
|
2412
3763
|
const existing = reactiveProxies.get(arr);
|
|
2413
3764
|
if (existing) {
|
|
@@ -2421,88 +3772,32 @@ function wrapArray(arr, report, pathSegs) {
|
|
|
2421
3772
|
kind: 'proxy',
|
|
2422
3773
|
};
|
|
2423
3774
|
addOwner(entry, report, pathSegs);
|
|
2424
|
-
|
|
2425
|
-
const item = arr[i];
|
|
2426
|
-
for (const o of entry.owners) {
|
|
2427
|
-
arr[i] = wrapReactive(item, o.report, [...o.baseSegs, String(i)]);
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
3775
|
+
const isArrayIndex = (prop) => typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
|
|
2430
3776
|
const proxy = new Proxy(arr, {
|
|
2431
3777
|
get(target, prop, receiver) {
|
|
2432
3778
|
if (typeof prop === 'string' && ARRAY_MUTATORS.has(prop)) {
|
|
2433
3779
|
const fn = target[prop];
|
|
2434
3780
|
return (...args) => {
|
|
2435
|
-
const
|
|
2436
|
-
if (prop === 'splice' && idx >= 2) {
|
|
2437
|
-
let w = a;
|
|
2438
|
-
for (const o of entry.owners) {
|
|
2439
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2440
|
-
}
|
|
2441
|
-
return w;
|
|
2442
|
-
}
|
|
2443
|
-
if ((prop === 'push' || prop === 'unshift') && typeof a === 'object') {
|
|
2444
|
-
let w = a;
|
|
2445
|
-
for (const o of entry.owners) {
|
|
2446
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2447
|
-
}
|
|
2448
|
-
return w;
|
|
2449
|
-
}
|
|
2450
|
-
if (prop === 'fill') {
|
|
2451
|
-
let w = a;
|
|
2452
|
-
for (const o of entry.owners) {
|
|
2453
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2454
|
-
}
|
|
2455
|
-
return w;
|
|
2456
|
-
}
|
|
2457
|
-
return a;
|
|
2458
|
-
});
|
|
2459
|
-
const ret = fn.apply(target, wrappedArgs);
|
|
3781
|
+
const ret = fn.apply(target, args);
|
|
2460
3782
|
notifyOwners(entry.owners, null);
|
|
2461
3783
|
return ret;
|
|
2462
3784
|
};
|
|
2463
3785
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
let nested = null;
|
|
2467
|
-
for (const o of entry.owners) {
|
|
2468
|
-
nested = wrapReactive(v, o.report, [...o.baseSegs, prop]);
|
|
2469
|
-
}
|
|
2470
|
-
return nested;
|
|
2471
|
-
}
|
|
2472
|
-
return v;
|
|
3786
|
+
// Indices / length / methods: return as-is (plain elements).
|
|
3787
|
+
return Reflect.get(target, prop, receiver);
|
|
2473
3788
|
},
|
|
2474
3789
|
set(target, prop, next, receiver) {
|
|
2475
|
-
const isIndex = typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
|
|
2476
|
-
const allRoot = entry.owners.every((o) => o.baseSegs.length === 0);
|
|
2477
|
-
if (isIndex && allRoot) {
|
|
2478
|
-
let wrapped = next;
|
|
2479
|
-
for (const o of entry.owners) {
|
|
2480
|
-
wrapped = wrapReactive(next, o.report, [...o.baseSegs, prop]);
|
|
2481
|
-
}
|
|
2482
|
-
const prev = target[prop];
|
|
2483
|
-
if (Object.is(prev, wrapped))
|
|
2484
|
-
return true;
|
|
2485
|
-
const ok = Reflect.set(target, prop, wrapped, receiver);
|
|
2486
|
-
if (ok)
|
|
2487
|
-
notifyOwners(entry.owners, null);
|
|
2488
|
-
return ok;
|
|
2489
|
-
}
|
|
2490
|
-
const local = prop === 'length' || typeof prop !== 'string' ? [] : [prop];
|
|
2491
|
-
let wrapped = next;
|
|
2492
|
-
if (prop !== 'length') {
|
|
2493
|
-
for (const o of entry.owners) {
|
|
2494
|
-
wrapped = wrapReactive(next, o.report, [...o.baseSegs, ...local]);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
3790
|
const prev = target[prop];
|
|
2498
|
-
if (Object.is(prev,
|
|
3791
|
+
if (Object.is(prev, next))
|
|
2499
3792
|
return true;
|
|
2500
|
-
const ok = Reflect.set(target, prop,
|
|
3793
|
+
const ok = Reflect.set(target, prop, next, receiver);
|
|
2501
3794
|
if (ok) {
|
|
2502
|
-
if (prop === 'length')
|
|
3795
|
+
if (prop === 'length' || isArrayIndex(prop))
|
|
2503
3796
|
notifyOwners(entry.owners, null);
|
|
3797
|
+
else if (typeof prop === 'string')
|
|
3798
|
+
notifyOwners(entry.owners, [prop]);
|
|
2504
3799
|
else
|
|
2505
|
-
notifyOwners(entry.owners,
|
|
3800
|
+
notifyOwners(entry.owners, null);
|
|
2506
3801
|
}
|
|
2507
3802
|
return ok;
|
|
2508
3803
|
},
|
|
@@ -2532,7 +3827,7 @@ function wrapArray(arr, report, pathSegs) {
|
|
|
2532
3827
|
* string form is transitional field-root alias for replace.
|
|
2533
3828
|
*/
|
|
2534
3829
|
function scheduleRefresh(inst, notice) {
|
|
2535
|
-
if (!inst || inst.__vmzDestroyed)
|
|
3830
|
+
if (!inst || inst.__vmzDestroyed || inst.__vmzQuiet)
|
|
2536
3831
|
return;
|
|
2537
3832
|
const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
|
|
2538
3833
|
if (!n || !n.root)
|
|
@@ -2635,9 +3930,10 @@ export async function flushPending(inst) {
|
|
|
2635
3930
|
jobs.push(...refreshBinding(inst, id, trie));
|
|
2636
3931
|
}
|
|
2637
3932
|
for (const key of binderKeysMatchingTrie(inst, trie)) {
|
|
2638
|
-
if (coveredDeps[key])
|
|
2639
|
-
|
|
2640
|
-
|
|
3933
|
+
if (coveredDeps[key] || (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length)) {
|
|
3934
|
+
// BindingId path already flushed IR patches for this dep.
|
|
3935
|
+
// Still run binder-only patches (bindComponentProp uses bindingId null).
|
|
3936
|
+
jobs.push(...refreshFieldBinderOnly(inst, key));
|
|
2641
3937
|
continue;
|
|
2642
3938
|
}
|
|
2643
3939
|
jobs.push(...refreshField(inst, key));
|
|
@@ -2930,6 +4226,32 @@ function refreshField(inst, field) {
|
|
|
2930
4226
|
}
|
|
2931
4227
|
return jobs;
|
|
2932
4228
|
}
|
|
4229
|
+
/**
|
|
4230
|
+
* Run `__vmzBinders` patches that are not owned by a BindingId entry.
|
|
4231
|
+
* Needed so `bindComponentProp` (bindingId null) still flushes when the same
|
|
4232
|
+
* dep also has IR bindText/bindAttr BindingIds.
|
|
4233
|
+
* @returns {Promise[]}
|
|
4234
|
+
*/
|
|
4235
|
+
function refreshFieldBinderOnly(inst, field) {
|
|
4236
|
+
const binders = inst.__vmzBinders;
|
|
4237
|
+
const jobs = [];
|
|
4238
|
+
if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
|
|
4239
|
+
return jobs;
|
|
4240
|
+
}
|
|
4241
|
+
for (const fn of binders[field]) {
|
|
4242
|
+
if (patchHasBindingId(inst, fn))
|
|
4243
|
+
continue;
|
|
4244
|
+
try {
|
|
4245
|
+
const ret = runPatch(fn, field, null);
|
|
4246
|
+
if (ret && typeof ret.then === 'function')
|
|
4247
|
+
jobs.push(ret);
|
|
4248
|
+
}
|
|
4249
|
+
catch (err) {
|
|
4250
|
+
console.error('vmz:dom patch', err);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
return jobs;
|
|
4254
|
+
}
|
|
2933
4255
|
/**
|
|
2934
4256
|
* @param {object} inst
|
|
2935
4257
|
* @param {number|string} bindingId
|
|
@@ -2963,7 +4285,7 @@ function reindexBindingDeps(inst, bindingId, deps) {
|
|
|
2963
4285
|
/**
|
|
2964
4286
|
* @param {object} inst
|
|
2965
4287
|
* @param {string[]} deps
|
|
2966
|
-
* @param { => any} fn
|
|
4288
|
+
* @param {() => any} fn
|
|
2967
4289
|
* @param {number|string|null|undefined} [bindingId]
|
|
2968
4290
|
*/
|
|
2969
4291
|
function registerBind(inst, deps, fn, bindingId = null) {
|
|
@@ -2990,7 +4312,7 @@ function registerBind(inst, deps, fn, bindingId = null) {
|
|
|
2990
4312
|
/**
|
|
2991
4313
|
* @param {object} inst
|
|
2992
4314
|
* @param {string[]} deps
|
|
2993
|
-
* @param { => any} fn
|
|
4315
|
+
* @param {() => any} fn
|
|
2994
4316
|
* @param {number|string|null|undefined} [bindingId]
|
|
2995
4317
|
*/
|
|
2996
4318
|
function unregisterBind(inst, deps, fn, bindingId = null) {
|