@vmz/core 0.3.9 → 0.3.11
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/direct-api.types.d.ts +10 -0
- package/dist/browser/dom/component.d.ts +15 -3
- package/dist/browser/dom/component.js +101 -19
- package/dist/browser/dom/direct-api.d.ts +28 -1
- package/dist/browser/dom/direct-api.js +85 -7
- package/dist/browser/dom-core.d.ts +1 -0
- package/dist/browser/dom-core.js +1 -0
- package/dist/browser/reactivity/patch-scheduler.js +42 -2
- package/dist/ssr/serialize.js +101 -47
- package/package.json +7 -7
|
@@ -99,6 +99,14 @@ export type ComponentCtorLike = (new (props?: object) => DirectInstance) & {
|
|
|
99
99
|
__vmzProps?: string[];
|
|
100
100
|
__vmzWBInstalled?: boolean;
|
|
101
101
|
__vmzCtorAppliesProps?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Compile-time embedded BindingId → deps[] map. When the Rust generator
|
|
104
|
+
* emits this static map on the component class, `registerBind` resolves
|
|
105
|
+
* deps by lookup instead of rebuilding O(N×M) indices from the string[]
|
|
106
|
+
* the artifact passes per call. Absent (today) — falls back to the call
|
|
107
|
+
* site argument unchanged.
|
|
108
|
+
*/
|
|
109
|
+
__vmzDepsMap?: Record<string | number, string[]>;
|
|
102
110
|
name?: string;
|
|
103
111
|
};
|
|
104
112
|
export type DirectEachCtx = {
|
|
@@ -134,7 +142,9 @@ export type DirectApi = {
|
|
|
134
142
|
adoptEnter: (node: Element) => boolean;
|
|
135
143
|
adoptLeave: () => void;
|
|
136
144
|
projectDefaultSlot: (hostEl: HTMLElement, node: Node) => void;
|
|
145
|
+
projectNamedSlot: (hostEl: HTMLElement, name: string, node: Node) => void;
|
|
137
146
|
setHtml: (el: Element, value: unknown) => void;
|
|
147
|
+
setShow: (el: HTMLElement, condition: unknown) => void;
|
|
138
148
|
bindHtml: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, el: Element) => void;
|
|
139
149
|
bindComponentProp: (inst: DirectInstance, bindingId: BindingId, deps: string[], get: PatchFn, hostInst: DirectInstance, propName: string) => void;
|
|
140
150
|
component: (Ctor: ComponentCtorLike | string, props?: object) => HTMLElement;
|
|
@@ -3,6 +3,14 @@ export declare function settlePendingChildMounts(inst: any): Promise<void>;
|
|
|
3
3
|
export declare function runDirectCreate(Component: any, inst: any): any;
|
|
4
4
|
export declare function findOwnedDefaultSlot(root: any): any;
|
|
5
5
|
export declare function findOwnedDefaultSlotTarget(root: any): any;
|
|
6
|
+
/**
|
|
7
|
+
* Owned `<slot name="…">` outlet for a named projection.
|
|
8
|
+
*
|
|
9
|
+
* Same ownership rule as {@link findOwnedDefaultSlot}: a nested component host
|
|
10
|
+
* (`[data-vmz]` / `__vmzInst`) is a boundary, so projecting `header` into a Card
|
|
11
|
+
* can never capture a Button's own named slot.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findOwnedNamedSlot(root: any, name: any): any;
|
|
6
14
|
export declare function createInstance(Component: any, props?: any): any;
|
|
7
15
|
export declare function destroy(inst: any): void;
|
|
8
16
|
/**
|
|
@@ -13,15 +21,19 @@ export declare function destroy(inst: any): void;
|
|
|
13
21
|
export declare function slotAnchorFor(node: any): any;
|
|
14
22
|
export declare function insertProjectedSlotNode(target: any, node: any): any;
|
|
15
23
|
/**
|
|
16
|
-
* Hold projected children until the component
|
|
24
|
+
* Hold projected children until the component slot exists.
|
|
17
25
|
*
|
|
18
26
|
* A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
|
|
19
27
|
* skeleton) is absent at projection time. Appending to the component root would
|
|
20
28
|
* paint the child into the live page, so keep it detached and splice it in when
|
|
21
29
|
* the slot is mounted.
|
|
30
|
+
*
|
|
31
|
+
* `name` parks for a specific `slot name="…"` outlet. Named projections never
|
|
32
|
+
* fall back into the default slot: a missing named outlet keeps the node parked
|
|
33
|
+
* (or drops it on hosts without MutationObserver) instead of polluting it.
|
|
22
34
|
*/
|
|
23
|
-
export declare function parkSlotNode(hostEl: any, node: any): void;
|
|
24
|
-
/** Move parked children into the now-mounted
|
|
35
|
+
export declare function parkSlotNode(hostEl: any, node: any, name?: any): void;
|
|
36
|
+
/** Move parked children into the now-mounted slot. Returns true when any drained. */
|
|
25
37
|
export declare function flushParkedSlotNodes(hostEl: any): boolean;
|
|
26
38
|
export declare function disposeDomTree(root: any): void;
|
|
27
39
|
export declare function snapshotInstanceState(inst: any): {};
|
|
@@ -120,6 +120,37 @@ export function findOwnedDefaultSlotTarget(root) {
|
|
|
120
120
|
};
|
|
121
121
|
return walk(root);
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Owned `<slot name="…">` outlet for a named projection.
|
|
125
|
+
*
|
|
126
|
+
* Same ownership rule as {@link findOwnedDefaultSlot}: a nested component host
|
|
127
|
+
* (`[data-vmz]` / `__vmzInst`) is a boundary, so projecting `header` into a Card
|
|
128
|
+
* can never capture a Button's own named slot.
|
|
129
|
+
*/
|
|
130
|
+
export function findOwnedNamedSlot(root, name) {
|
|
131
|
+
if (!root || root.nodeType !== 1)
|
|
132
|
+
return null;
|
|
133
|
+
if (name == null || name === '')
|
|
134
|
+
return null;
|
|
135
|
+
const wanted = String(name);
|
|
136
|
+
const tag = String(root.tagName || '').toLowerCase();
|
|
137
|
+
if (tag === 'slot' && root.getAttribute('name') === wanted)
|
|
138
|
+
return root;
|
|
139
|
+
const kids = root.children;
|
|
140
|
+
if (!kids || !kids.length)
|
|
141
|
+
return null;
|
|
142
|
+
for (let i = 0; i < kids.length; i++) {
|
|
143
|
+
const c = kids[i];
|
|
144
|
+
if (c.nodeType !== 1)
|
|
145
|
+
continue;
|
|
146
|
+
if (c.hasAttribute('data-vmz') || c.__vmzInst)
|
|
147
|
+
continue;
|
|
148
|
+
const hit = findOwnedNamedSlot(c, wanted);
|
|
149
|
+
if (hit)
|
|
150
|
+
return hit;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
123
154
|
export function createInstance(Component, props = {}) {
|
|
124
155
|
if (precision.enabled)
|
|
125
156
|
precision.componentExecs++;
|
|
@@ -203,17 +234,24 @@ export function insertProjectedSlotNode(target, node) {
|
|
|
203
234
|
return anchor;
|
|
204
235
|
}
|
|
205
236
|
/**
|
|
206
|
-
* Hold projected children until the component
|
|
237
|
+
* Hold projected children until the component slot exists.
|
|
207
238
|
*
|
|
208
239
|
* A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
|
|
209
240
|
* skeleton) is absent at projection time. Appending to the component root would
|
|
210
241
|
* paint the child into the live page, so keep it detached and splice it in when
|
|
211
242
|
* the slot is mounted.
|
|
243
|
+
*
|
|
244
|
+
* `name` parks for a specific `slot name="…"` outlet. Named projections never
|
|
245
|
+
* fall back into the default slot: a missing named outlet keeps the node parked
|
|
246
|
+
* (or drops it on hosts without MutationObserver) instead of polluting it.
|
|
212
247
|
*/
|
|
213
|
-
export function parkSlotNode(hostEl, node) {
|
|
248
|
+
export function parkSlotNode(hostEl, node, name = null) {
|
|
214
249
|
if (!hostEl || !node)
|
|
215
250
|
return;
|
|
251
|
+
const slotName = name == null ? null : String(name);
|
|
216
252
|
if (typeof MutationObserver === 'undefined') {
|
|
253
|
+
if (slotName != null)
|
|
254
|
+
return;
|
|
217
255
|
// Non-browser DOM (unit hosts): preserve the legacy root fallback.
|
|
218
256
|
const child = hostEl.__vmzInst;
|
|
219
257
|
const root = (child && child.__vmzDomRoot) || hostEl;
|
|
@@ -223,8 +261,28 @@ export function parkSlotNode(hostEl, node) {
|
|
|
223
261
|
hostEl.appendChild(node);
|
|
224
262
|
return;
|
|
225
263
|
}
|
|
226
|
-
|
|
227
|
-
|
|
264
|
+
if (slotName == null) {
|
|
265
|
+
const parked = hostEl.__vmzSlotParked || (hostEl.__vmzSlotParked = []);
|
|
266
|
+
parked.push(node);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
namedSlotParked(hostEl, slotName).push(node);
|
|
270
|
+
}
|
|
271
|
+
ensureSlotObserver(hostEl);
|
|
272
|
+
}
|
|
273
|
+
function namedSlotParked(hostEl, name) {
|
|
274
|
+
const map = hostEl.__vmzNamedSlotParked || (hostEl.__vmzNamedSlotParked = new Map());
|
|
275
|
+
const list = map.get(name);
|
|
276
|
+
if (list)
|
|
277
|
+
return list;
|
|
278
|
+
const fresh = [];
|
|
279
|
+
map.set(name, fresh);
|
|
280
|
+
return fresh;
|
|
281
|
+
}
|
|
282
|
+
function namedSlotAnchor(hostEl) {
|
|
283
|
+
return hostEl.__vmzNamedSlotAnchor || (hostEl.__vmzNamedSlotAnchor = new Map());
|
|
284
|
+
}
|
|
285
|
+
function ensureSlotObserver(hostEl) {
|
|
228
286
|
if (hostEl.__vmzSlotObserver)
|
|
229
287
|
return;
|
|
230
288
|
hostEl.__vmzSlotObserver = new MutationObserver(() => {
|
|
@@ -235,44 +293,64 @@ export function parkSlotNode(hostEl, node) {
|
|
|
235
293
|
hostEl.__vmzSlotObserver.observe(scope, { childList: true, subtree: true });
|
|
236
294
|
}
|
|
237
295
|
}
|
|
238
|
-
/** Move parked children into the now-mounted
|
|
296
|
+
/** Move parked children into the now-mounted slot. Returns true when any drained. */
|
|
239
297
|
export function flushParkedSlotNodes(hostEl) {
|
|
240
298
|
if (!hostEl)
|
|
241
299
|
return false;
|
|
242
|
-
const parked = hostEl.__vmzSlotParked;
|
|
243
|
-
if (!parked || !parked.length)
|
|
244
|
-
return false;
|
|
245
300
|
const child = hostEl.__vmzInst;
|
|
246
301
|
const root = (child && child.__vmzDomRoot) || hostEl;
|
|
247
302
|
if (!root || root.nodeType !== 1)
|
|
248
303
|
return false;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
let drained = false;
|
|
305
|
+
const parkedDefault = hostEl.__vmzSlotParked;
|
|
306
|
+
if (parkedDefault && parkedDefault.length) {
|
|
307
|
+
const target = findOwnedDefaultSlotTarget(root);
|
|
308
|
+
if (target && target.parentNode) {
|
|
309
|
+
hostEl.__vmzSlotParked = [];
|
|
310
|
+
hostEl.__vmzSlotAnchor = insertParked(hostEl, target, parkedDefault);
|
|
311
|
+
drained = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const named = hostEl.__vmzNamedSlotParked;
|
|
315
|
+
if (named && named.size) {
|
|
316
|
+
for (const [name, nodes] of named) {
|
|
317
|
+
if (!nodes.length) {
|
|
318
|
+
named.delete(name);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const target = findOwnedNamedSlot(root, name);
|
|
322
|
+
if (!target || !target.parentNode)
|
|
323
|
+
continue;
|
|
324
|
+
named.delete(name);
|
|
325
|
+
namedSlotAnchor(hostEl).set(name, insertParked(hostEl, target, nodes));
|
|
326
|
+
drained = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const stillParked = (hostEl.__vmzSlotParked && hostEl.__vmzSlotParked.length > 0) ||
|
|
330
|
+
(hostEl.__vmzNamedSlotParked && hostEl.__vmzNamedSlotParked.size > 0);
|
|
331
|
+
if (drained && !stillParked && hostEl.__vmzSlotObserver) {
|
|
253
332
|
hostEl.__vmzSlotObserver.disconnect();
|
|
254
333
|
hostEl.__vmzSlotObserver = null;
|
|
255
334
|
}
|
|
256
|
-
|
|
335
|
+
return drained;
|
|
336
|
+
}
|
|
337
|
+
function insertParked(hostEl, target, parked) {
|
|
257
338
|
const anchor = slotAnchorFor(parked[parked.length - 1]);
|
|
258
339
|
const tag = String(target.tagName || '').toLowerCase();
|
|
259
340
|
if (tag === 'slot') {
|
|
260
341
|
const frag = typeof document !== 'undefined' ? document.createDocumentFragment() : null;
|
|
261
342
|
if (!frag) {
|
|
262
343
|
target.replaceWith(parked[0]);
|
|
263
|
-
|
|
264
|
-
return true;
|
|
344
|
+
return anchor;
|
|
265
345
|
}
|
|
266
346
|
for (const node of parked)
|
|
267
347
|
frag.appendChild(node);
|
|
268
348
|
target.replaceWith(frag);
|
|
269
|
-
|
|
270
|
-
return true;
|
|
349
|
+
return anchor;
|
|
271
350
|
}
|
|
272
351
|
for (const node of parked)
|
|
273
352
|
target.appendChild(node);
|
|
274
|
-
|
|
275
|
-
return true;
|
|
353
|
+
return anchor;
|
|
276
354
|
}
|
|
277
355
|
export function disposeDomTree(root) {
|
|
278
356
|
if (!root)
|
|
@@ -304,6 +382,10 @@ export function disposeDomTree(root) {
|
|
|
304
382
|
node.__vmzSlotParked = [];
|
|
305
383
|
if (node.__vmzSlotAnchor)
|
|
306
384
|
node.__vmzSlotAnchor = null;
|
|
385
|
+
if (node.__vmzNamedSlotParked)
|
|
386
|
+
node.__vmzNamedSlotParked = null;
|
|
387
|
+
if (node.__vmzNamedSlotAnchor)
|
|
388
|
+
node.__vmzNamedSlotAnchor = null;
|
|
307
389
|
let child = node.firstChild;
|
|
308
390
|
while (child) {
|
|
309
391
|
const next = child.nextSibling;
|
|
@@ -30,7 +30,7 @@ export declare const directApi: {
|
|
|
30
30
|
attr(el: any, name: any, value: any): void;
|
|
31
31
|
mergeClass(...parts: any[]): string;
|
|
32
32
|
mergeStyle(...parts: any[]): string;
|
|
33
|
-
on(el: any, type: any, handler: any): void;
|
|
33
|
+
on(el: any, type: any, handler: any, options?: any): void;
|
|
34
34
|
/**
|
|
35
35
|
* Bind a named instance method (no arrow / Function#toString).
|
|
36
36
|
* Learns `skipFlush` after a sync invocation that schedules no dirty work
|
|
@@ -46,7 +46,25 @@ export declare const directApi: {
|
|
|
46
46
|
*/
|
|
47
47
|
specFieldText(inst: any, bindingId: any, fieldName: any, textNode: any): void;
|
|
48
48
|
setHtml(el: any, value: any): void;
|
|
49
|
+
/**
|
|
50
|
+
* Vue `v-text` — sets `textContent`, escaping all HTML (unlike `setHtml`).
|
|
51
|
+
* Overrides any existing children on the element. Static create-time path;
|
|
52
|
+
* reactive bindings use {@link bindText}.
|
|
53
|
+
*/
|
|
54
|
+
setText(el: any, value: any): void;
|
|
55
|
+
/**
|
|
56
|
+
* Vue `v-show` — toggles `style.display` between empty string (visible,
|
|
57
|
+
* original CSS wins) and `'none'`. Element stays in the DOM tree (unlike
|
|
58
|
+
* `v-if`). Used for static-show values; reactive bindings emit inline
|
|
59
|
+
* patches in {@link trackPatch}.
|
|
60
|
+
*/
|
|
61
|
+
setShow(el: any, condition: any): void;
|
|
49
62
|
bindHtml(inst: any, bindingId: any, deps: any, get: any, el: any): void;
|
|
63
|
+
/**
|
|
64
|
+
* Reactive `v-text` textContent binding — mirrors {@link bindHtml} but
|
|
65
|
+
* writes `textContent` (HTML-escaped, safe).
|
|
66
|
+
*/
|
|
67
|
+
bindText(inst: any, bindingId: any, deps: any, get: any, el: any): void;
|
|
50
68
|
/**
|
|
51
69
|
* Nested component (sync Direct child or island schedule).
|
|
52
70
|
*/
|
|
@@ -65,4 +83,13 @@ export declare const directApi: {
|
|
|
65
83
|
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|
|
66
84
|
*/
|
|
67
85
|
projectDefaultSlot(hostEl: any, node: any): void;
|
|
86
|
+
/**
|
|
87
|
+
* Project parent children into a nested Direct component's `<slot name="…">`.
|
|
88
|
+
*
|
|
89
|
+
* Named projections are name-only (scope-slot values are not passed yet). The
|
|
90
|
+
* outlet search stops at nested component hosts, so a `header` projection never
|
|
91
|
+
* steals a nested Button's own named slot. A name with no outlet parks under that
|
|
92
|
+
* name — it must not fall through into the default slot.
|
|
93
|
+
*/
|
|
94
|
+
projectNamedSlot(hostEl: any, name: any, node: any): void;
|
|
68
95
|
};
|
|
@@ -2,7 +2,7 @@ import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normaliz
|
|
|
2
2
|
import { applyDirectHostBox } from '../direct-host-box.js';
|
|
3
3
|
import { createUnknownComponentElement } from '../unknown-component.js';
|
|
4
4
|
import { applyDomAttr, eventPropHandlerName } from './attributes.js';
|
|
5
|
-
import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
|
|
5
|
+
import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
|
|
6
6
|
import { isEventEntryStrategy, noteDomCreate, noteDomRemove, scheduleClientOn } from './lifecycle.js';
|
|
7
7
|
import { components, resolveComponent } from './registry.js';
|
|
8
8
|
import { beginEventFlush, endEventFlush, inferHandlerMethod, methodAllowsSyncEventFlush, registerBind, runDomEventHandler, runPatch, scheduleRefresh, unregisterBind, } from '../reactivity/patch-scheduler.js';
|
|
@@ -169,7 +169,11 @@ export const directApi = {
|
|
|
169
169
|
mergeStyle(...parts) {
|
|
170
170
|
return mergeStyleParts(...parts);
|
|
171
171
|
},
|
|
172
|
-
on(el, type, handler) {
|
|
172
|
+
on(el, type, handler, options) {
|
|
173
|
+
// options: { capture?: boolean; passive?: boolean; once?: boolean }
|
|
174
|
+
const listenerOpts = options && (options.capture || options.passive || options.once)
|
|
175
|
+
? { capture: !!options.capture, passive: !!options.passive, once: !!options.once }
|
|
176
|
+
: undefined;
|
|
173
177
|
const inst = directApi._inst;
|
|
174
178
|
if (directApi._eachCtx && typeof handler === 'function') {
|
|
175
179
|
const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
|
|
@@ -191,7 +195,7 @@ export const directApi = {
|
|
|
191
195
|
};
|
|
192
196
|
bag[type] = listener;
|
|
193
197
|
directApi._eachCtx.needDelegate(type);
|
|
194
|
-
el.addEventListener(type, listener);
|
|
198
|
+
el.addEventListener(type, listener, listenerOpts);
|
|
195
199
|
return;
|
|
196
200
|
}
|
|
197
201
|
// Infer once at bind time ??never Function#toString on the click hot path.
|
|
@@ -208,7 +212,7 @@ export const directApi = {
|
|
|
208
212
|
return m.call(inst, ev);
|
|
209
213
|
return handler.call(inst, ev);
|
|
210
214
|
});
|
|
211
|
-
});
|
|
215
|
+
}, listenerOpts);
|
|
212
216
|
return;
|
|
213
217
|
}
|
|
214
218
|
el.addEventListener(type, (ev) => {
|
|
@@ -219,7 +223,7 @@ export const directApi = {
|
|
|
219
223
|
if (typeof handler === 'function') {
|
|
220
224
|
runDomEventHandler(inst, methodHint, () => handler.call(inst, ev));
|
|
221
225
|
}
|
|
222
|
-
});
|
|
226
|
+
}, listenerOpts);
|
|
223
227
|
},
|
|
224
228
|
/**
|
|
225
229
|
* Bind a named instance method (no arrow / Function#toString).
|
|
@@ -227,6 +231,10 @@ export const directApi = {
|
|
|
227
231
|
* (stride / transpose self-apply DOM).
|
|
228
232
|
*/
|
|
229
233
|
onMethod(el, type, methodName, opts) {
|
|
234
|
+
// opts: { skipFlush?: boolean; capture?: boolean; passive?: boolean; once?: boolean }
|
|
235
|
+
const listenerOpts = opts && (opts.capture || opts.passive || opts.once)
|
|
236
|
+
? { capture: !!opts.capture, passive: !!opts.passive, once: !!opts.once }
|
|
237
|
+
: undefined;
|
|
230
238
|
// Prefer each-block owner: item create often runs during flush when `_inst` is unset.
|
|
231
239
|
const eachOwner = directApi._eachCtx && directApi._eachCtx.inst;
|
|
232
240
|
const inst = eachOwner || directApi._inst;
|
|
@@ -262,10 +270,10 @@ export const directApi = {
|
|
|
262
270
|
const listener = (ev) => invoke(ev);
|
|
263
271
|
bag[type] = listener;
|
|
264
272
|
directApi._eachCtx.needDelegate(type);
|
|
265
|
-
el.addEventListener(type, listener);
|
|
273
|
+
el.addEventListener(type, listener, listenerOpts);
|
|
266
274
|
return;
|
|
267
275
|
}
|
|
268
|
-
el.addEventListener(type, (ev) => invoke(ev));
|
|
276
|
+
el.addEventListener(type, (ev) => invoke(ev), listenerOpts);
|
|
269
277
|
},
|
|
270
278
|
/**
|
|
271
279
|
* Specialized single-field attr bind (0.1.29): compile-time field name, no generic get closure in artifact.
|
|
@@ -303,6 +311,23 @@ export const directApi = {
|
|
|
303
311
|
setHtml(el, value) {
|
|
304
312
|
el.innerHTML = value == null ? '' : String(value);
|
|
305
313
|
},
|
|
314
|
+
/**
|
|
315
|
+
* Vue `v-text` — sets `textContent`, escaping all HTML (unlike `setHtml`).
|
|
316
|
+
* Overrides any existing children on the element. Static create-time path;
|
|
317
|
+
* reactive bindings use {@link bindText}.
|
|
318
|
+
*/
|
|
319
|
+
setText(el, value) {
|
|
320
|
+
el.textContent = value == null ? '' : String(value);
|
|
321
|
+
},
|
|
322
|
+
/**
|
|
323
|
+
* Vue `v-show` — toggles `style.display` between empty string (visible,
|
|
324
|
+
* original CSS wins) and `'none'`. Element stays in the DOM tree (unlike
|
|
325
|
+
* `v-if`). Used for static-show values; reactive bindings emit inline
|
|
326
|
+
* patches in {@link trackPatch}.
|
|
327
|
+
*/
|
|
328
|
+
setShow(el, condition) {
|
|
329
|
+
el.style.display = condition ? '' : 'none';
|
|
330
|
+
},
|
|
306
331
|
bindHtml(inst, bindingId, deps, get, el) {
|
|
307
332
|
directApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
|
|
308
333
|
let raw;
|
|
@@ -315,6 +340,22 @@ export const directApi = {
|
|
|
315
340
|
el.innerHTML = raw == null ? '' : String(raw);
|
|
316
341
|
}, bindingId);
|
|
317
342
|
},
|
|
343
|
+
/**
|
|
344
|
+
* Reactive `v-text` textContent binding — mirrors {@link bindHtml} but
|
|
345
|
+
* writes `textContent` (HTML-escaped, safe).
|
|
346
|
+
*/
|
|
347
|
+
bindText(inst, bindingId, deps, get, el) {
|
|
348
|
+
directApi.trackPatch(inst, deps || [], function bindTextPatch() {
|
|
349
|
+
let raw;
|
|
350
|
+
try {
|
|
351
|
+
raw = get.call(inst);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
raw = '';
|
|
355
|
+
}
|
|
356
|
+
el.textContent = raw == null ? '' : String(raw);
|
|
357
|
+
}, bindingId);
|
|
358
|
+
},
|
|
318
359
|
/**
|
|
319
360
|
* Nested component (sync Direct child or island schedule).
|
|
320
361
|
*/
|
|
@@ -483,4 +524,41 @@ export const directApi = {
|
|
|
483
524
|
// the component root, where it would otherwise leak into the rendered page.
|
|
484
525
|
parkSlotNode(hostEl, node);
|
|
485
526
|
},
|
|
527
|
+
/**
|
|
528
|
+
* Project parent children into a nested Direct component's `<slot name="…">`.
|
|
529
|
+
*
|
|
530
|
+
* Named projections are name-only (scope-slot values are not passed yet). The
|
|
531
|
+
* outlet search stops at nested component hosts, so a `header` projection never
|
|
532
|
+
* steals a nested Button's own named slot. A name with no outlet parks under that
|
|
533
|
+
* name — it must not fall through into the default slot.
|
|
534
|
+
*/
|
|
535
|
+
projectNamedSlot(hostEl, name, node) {
|
|
536
|
+
if (!hostEl || node == null)
|
|
537
|
+
return;
|
|
538
|
+
const key = name == null ? '' : String(name);
|
|
539
|
+
if (!key || key === 'default') {
|
|
540
|
+
directApi.projectDefaultSlot(hostEl, node);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const child = hostEl.__vmzInst;
|
|
544
|
+
let root = (child && child.__vmzDomRoot) || hostEl;
|
|
545
|
+
if (!root || root.nodeType !== 1)
|
|
546
|
+
root = hostEl;
|
|
547
|
+
const target = root && root.nodeType === 1 ? findOwnedNamedSlot(root, key) : null;
|
|
548
|
+
if (target && target.parentNode) {
|
|
549
|
+
const anchors = hostEl.__vmzNamedSlotAnchor || (hostEl.__vmzNamedSlotAnchor = new Map());
|
|
550
|
+
anchors.set(key, insertProjectedSlotNode(target, node));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// The named `<slot>` was replaced by an earlier projected child: append after
|
|
554
|
+
// the last one for the same name instead of re-searching.
|
|
555
|
+
const anchors = hostEl.__vmzNamedSlotAnchor;
|
|
556
|
+
const anchor = anchors && anchors.get(key);
|
|
557
|
+
if (anchor && anchor.parentNode) {
|
|
558
|
+
anchors.set(key, slotAnchorFor(node));
|
|
559
|
+
anchor.parentNode.insertBefore(node, anchor.nextSibling);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
parkSlotNode(hostEl, node, key);
|
|
563
|
+
},
|
|
486
564
|
};
|
|
@@ -16,6 +16,7 @@ export { settlePendingChildMounts } from './dom/component.js';
|
|
|
16
16
|
export { runDirectCreate } from './dom/component.js';
|
|
17
17
|
export { findOwnedDefaultSlot } from './dom/component.js';
|
|
18
18
|
export { findOwnedDefaultSlotTarget } from './dom/component.js';
|
|
19
|
+
export { findOwnedNamedSlot } from './dom/component.js';
|
|
19
20
|
export { createInstance } from './dom/component.js';
|
|
20
21
|
export { destroy } from './dom/component.js';
|
|
21
22
|
export { disposeDomTree } from './dom/component.js';
|
package/dist/browser/dom-core.js
CHANGED
|
@@ -16,6 +16,7 @@ export { settlePendingChildMounts } from './dom/component.js';
|
|
|
16
16
|
export { runDirectCreate } from './dom/component.js';
|
|
17
17
|
export { findOwnedDefaultSlot } from './dom/component.js';
|
|
18
18
|
export { findOwnedDefaultSlotTarget } from './dom/component.js';
|
|
19
|
+
export { findOwnedNamedSlot } from './dom/component.js';
|
|
19
20
|
export { createInstance } from './dom/component.js';
|
|
20
21
|
export { destroy } from './dom/component.js';
|
|
21
22
|
export { disposeDomTree } from './dom/component.js';
|
|
@@ -645,6 +645,9 @@ function reindexBindingDeps(inst, bindingId, deps) {
|
|
|
645
645
|
const entry = inst.__vmzBindings[bindingId];
|
|
646
646
|
if (!entry)
|
|
647
647
|
return;
|
|
648
|
+
// Fast path: deps 完全相同 → 无需重建 dep→bindings 索引。
|
|
649
|
+
if (arraysEqual(entry.deps, deps))
|
|
650
|
+
return;
|
|
648
651
|
for (const dep of entry.deps || []) {
|
|
649
652
|
const list = inst.__vmzDepToBindings[dep];
|
|
650
653
|
if (!list)
|
|
@@ -664,10 +667,47 @@ function reindexBindingDeps(inst, bindingId, deps) {
|
|
|
664
667
|
}
|
|
665
668
|
}
|
|
666
669
|
}
|
|
670
|
+
function arraysEqual(a, b) {
|
|
671
|
+
if (a === b)
|
|
672
|
+
return true;
|
|
673
|
+
const la = a ? a.length : 0;
|
|
674
|
+
const lb = b ? b.length : 0;
|
|
675
|
+
if (la !== lb)
|
|
676
|
+
return false;
|
|
677
|
+
for (let i = 0; i < la; i++)
|
|
678
|
+
if (a[i] !== b[i])
|
|
679
|
+
return false;
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Resolve deps for a binding: prefer the argument from generated artifact,
|
|
684
|
+
* fall back to a compile-time-embedded static map on the component class.
|
|
685
|
+
*
|
|
686
|
+
* The Rust generator will later emit `Ctor.__vmzDepsMap = { [bindingId]: deps[] }`
|
|
687
|
+
* so runtime can do a single lookup instead of rebuilding O(N×M) dep→binding
|
|
688
|
+
* indices from the string[] the artifact passes today.
|
|
689
|
+
*/
|
|
690
|
+
function resolveDeps(inst, depsArg, bindingId) {
|
|
691
|
+
if (depsArg && depsArg.length)
|
|
692
|
+
return depsArg;
|
|
693
|
+
if (bindingId == null)
|
|
694
|
+
return depsArg || [];
|
|
695
|
+
const ctor = inst && inst.constructor;
|
|
696
|
+
const map = ctor && ctor.__vmzDepsMap;
|
|
697
|
+
if (map) {
|
|
698
|
+
const hit = map[bindingId];
|
|
699
|
+
if (hit)
|
|
700
|
+
return hit;
|
|
701
|
+
}
|
|
702
|
+
return depsArg || [];
|
|
703
|
+
}
|
|
667
704
|
export function registerBind(inst, deps, fn, bindingId = null) {
|
|
705
|
+
// 查表优先: generator 侧若已在 Ctor.__vmzDepsMap 嵌入完整 deps map
|
|
706
|
+
// 则直接取用, 避免每次 registerBind 都用传入 string[] 重建索引.
|
|
707
|
+
const resolvedDeps = resolveDeps(inst, deps, bindingId);
|
|
668
708
|
if (!inst.__vmzBinders)
|
|
669
709
|
inst.__vmzBinders = Object.create(null);
|
|
670
|
-
for (const dep of
|
|
710
|
+
for (const dep of resolvedDeps) {
|
|
671
711
|
if (!inst.__vmzBinders[dep])
|
|
672
712
|
inst.__vmzBinders[dep] = [];
|
|
673
713
|
// Dedup: cf untrack/retrack with identical deps must not grow the binder list.
|
|
@@ -685,7 +725,7 @@ export function registerBind(inst, deps, fn, bindingId = null) {
|
|
|
685
725
|
}
|
|
686
726
|
if (!entry.patches.includes(fn))
|
|
687
727
|
entry.patches.push(fn);
|
|
688
|
-
reindexBindingDeps(inst, bindingId,
|
|
728
|
+
reindexBindingDeps(inst, bindingId, resolvedDeps);
|
|
689
729
|
}
|
|
690
730
|
export function unregisterBind(inst, deps, fn, bindingId = null) {
|
|
691
731
|
const binders = inst.__vmzBinders;
|
package/dist/ssr/serialize.js
CHANGED
|
@@ -307,6 +307,64 @@ function makeVirtualEl(tag) {
|
|
|
307
307
|
});
|
|
308
308
|
return node;
|
|
309
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Owned `<slot>` outlet in a serialize el tree.
|
|
312
|
+
*
|
|
313
|
+
* `name === null` matches the default outlet (no `name` attr). Named lookups require
|
|
314
|
+
* an exact match and stop at nested component shells (`data-vmz`), mirroring the
|
|
315
|
+
* browser runtime ownership boundary.
|
|
316
|
+
*/
|
|
317
|
+
function findOwnedSerializeSlot(n, name) {
|
|
318
|
+
if (!n || typeof n !== 'object')
|
|
319
|
+
return null;
|
|
320
|
+
if (n.__kind === 'frag') {
|
|
321
|
+
for (const c of n.children || []) {
|
|
322
|
+
const hit = findOwnedSerializeSlot(c, name);
|
|
323
|
+
if (hit)
|
|
324
|
+
return hit;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (n.__kind !== 'el')
|
|
329
|
+
return null;
|
|
330
|
+
if (n.tag === 'slot') {
|
|
331
|
+
const outletName = n.attrs && n.attrs.name != null ? String(n.attrs.name) : '';
|
|
332
|
+
if (name == null ? !outletName : outletName === name)
|
|
333
|
+
return n;
|
|
334
|
+
}
|
|
335
|
+
for (const c of n.children || []) {
|
|
336
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
337
|
+
continue;
|
|
338
|
+
const hit = findOwnedSerializeSlot(c, name);
|
|
339
|
+
if (hit)
|
|
340
|
+
return hit;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function findOwnedSerializeSlotTarget(n, name) {
|
|
345
|
+
const slot = findOwnedSerializeSlot(n, name);
|
|
346
|
+
if (slot)
|
|
347
|
+
return slot;
|
|
348
|
+
if (name != null)
|
|
349
|
+
return null;
|
|
350
|
+
if (!n || n.__kind !== 'el')
|
|
351
|
+
return null;
|
|
352
|
+
const tag = String(n.tag || '').toLowerCase();
|
|
353
|
+
if ((tag === 'button' || tag === 'a') && n.attrs && n.attrs['data-vmz-ui'] === 'button')
|
|
354
|
+
return n;
|
|
355
|
+
if (n.attrs && n.attrs['data-vmz'] != null)
|
|
356
|
+
return null;
|
|
357
|
+
for (const c of n.children || []) {
|
|
358
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
359
|
+
continue;
|
|
360
|
+
const hit = findOwnedSerializeSlotTarget(c, name);
|
|
361
|
+
if (hit)
|
|
362
|
+
return hit;
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
/** Outlets that already received projected content (fallback children dropped once). */
|
|
367
|
+
const filledSerializeSlots = new WeakSet();
|
|
310
368
|
const serializeApi = {
|
|
311
369
|
_inst: null,
|
|
312
370
|
_branchBinds: null,
|
|
@@ -476,69 +534,25 @@ const serializeApi = {
|
|
|
476
534
|
return;
|
|
477
535
|
// serializeApi.component returns a serialize el tree (or island shell).
|
|
478
536
|
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
479
|
-
const findOwnedSlot = (n) => {
|
|
480
|
-
if (!n || typeof n !== 'object')
|
|
481
|
-
return null;
|
|
482
|
-
if (n.__kind === 'frag') {
|
|
483
|
-
for (const c of n.children || []) {
|
|
484
|
-
const hit = findOwnedSlot(c);
|
|
485
|
-
if (hit)
|
|
486
|
-
return hit;
|
|
487
|
-
}
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
if (n.__kind !== 'el')
|
|
491
|
-
return null;
|
|
492
|
-
if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
|
|
493
|
-
return n;
|
|
494
|
-
for (const c of n.children || []) {
|
|
495
|
-
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
496
|
-
continue;
|
|
497
|
-
const hit = findOwnedSlot(c);
|
|
498
|
-
if (hit)
|
|
499
|
-
return hit;
|
|
500
|
-
}
|
|
501
|
-
return null;
|
|
502
|
-
};
|
|
503
|
-
const findOwnedSlotTarget = (n) => {
|
|
504
|
-
const slot = findOwnedSlot(n);
|
|
505
|
-
if (slot)
|
|
506
|
-
return slot;
|
|
507
|
-
if (!n || n.__kind !== 'el')
|
|
508
|
-
return null;
|
|
509
|
-
const tag = String(n.tag || '').toLowerCase();
|
|
510
|
-
if ((tag === 'button' || tag === 'a') && n.attrs && n.attrs['data-vmz-ui'] === 'button')
|
|
511
|
-
return n;
|
|
512
|
-
if (n.attrs && n.attrs['data-vmz'] != null)
|
|
513
|
-
return null;
|
|
514
|
-
for (const c of n.children || []) {
|
|
515
|
-
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
516
|
-
continue;
|
|
517
|
-
const hit = findOwnedSlotTarget(c);
|
|
518
|
-
if (hit)
|
|
519
|
-
return hit;
|
|
520
|
-
}
|
|
521
|
-
return null;
|
|
522
|
-
};
|
|
523
537
|
// Prefer searching the component body (first child of host wrapper).
|
|
524
538
|
let target = null;
|
|
525
539
|
if (root) {
|
|
526
540
|
for (const c of root.children || []) {
|
|
527
541
|
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
528
542
|
continue;
|
|
529
|
-
target =
|
|
543
|
+
target = findOwnedSerializeSlotTarget(c, null);
|
|
530
544
|
if (target)
|
|
531
545
|
break;
|
|
532
546
|
}
|
|
533
547
|
if (!target)
|
|
534
|
-
target =
|
|
548
|
+
target = findOwnedSerializeSlotTarget(root, null);
|
|
535
549
|
}
|
|
536
550
|
if (target) {
|
|
537
551
|
if (target.tag === 'slot') {
|
|
538
552
|
target.__rawHtml = null;
|
|
539
553
|
if (!Array.isArray(target.children))
|
|
540
554
|
target.children = [];
|
|
541
|
-
// Append
|
|
555
|
+
// Append — multiple projectDefaultSlot calls must accumulate (SSR).
|
|
542
556
|
// Client path replaces the live <slot> then appends siblings; serialize must push.
|
|
543
557
|
target.children.push(node);
|
|
544
558
|
return;
|
|
@@ -552,6 +566,46 @@ const serializeApi = {
|
|
|
552
566
|
// mounted (closed overlay, loading skeleton). Mirror the browser runtime and
|
|
553
567
|
// drop them instead of painting them into the component root.
|
|
554
568
|
},
|
|
569
|
+
/**
|
|
570
|
+
* Project parent children into a nested component's `<slot name="…">` outlet.
|
|
571
|
+
*
|
|
572
|
+
* Named projections are name-only (scope-slot values are not passed yet). Only an
|
|
573
|
+
* outlet with the exact `name` accepts the node — a missing name drops it instead
|
|
574
|
+
* of landing in the default slot. Fallback children stop rendering once the outlet
|
|
575
|
+
* has projected content.
|
|
576
|
+
*/
|
|
577
|
+
projectNamedSlot(hostEl, name, node) {
|
|
578
|
+
if (!hostEl || node == null)
|
|
579
|
+
return;
|
|
580
|
+
const key = name == null ? '' : String(name);
|
|
581
|
+
if (!key || key === 'default') {
|
|
582
|
+
serializeApi.projectDefaultSlot(hostEl, node);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
586
|
+
let target = null;
|
|
587
|
+
if (root) {
|
|
588
|
+
for (const c of root.children || []) {
|
|
589
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
590
|
+
continue;
|
|
591
|
+
target = findOwnedSerializeSlotTarget(c, key);
|
|
592
|
+
if (target)
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if (!target)
|
|
596
|
+
target = findOwnedSerializeSlotTarget(root, key);
|
|
597
|
+
}
|
|
598
|
+
if (!target)
|
|
599
|
+
return;
|
|
600
|
+
target.__rawHtml = null;
|
|
601
|
+
if (!Array.isArray(target.children))
|
|
602
|
+
target.children = [];
|
|
603
|
+
if (!filledSerializeSlots.has(target)) {
|
|
604
|
+
filledSerializeSlots.add(target);
|
|
605
|
+
target.children.length = 0;
|
|
606
|
+
}
|
|
607
|
+
target.children.push(node);
|
|
608
|
+
},
|
|
555
609
|
setHtml(el, value) {
|
|
556
610
|
if (!el || el.__kind !== 'el')
|
|
557
611
|
return;
|
package/package.json
CHANGED
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
"main": "./dist/faces/server.js",
|
|
69
69
|
"name": "@vmz/core",
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@vmz/vmz-darwin-arm64": "0.3.
|
|
72
|
-
"@vmz/vmz-darwin-x64": "0.3.
|
|
73
|
-
"@vmz/vmz-linux-arm64": "0.3.
|
|
74
|
-
"@vmz/vmz-linux-x64": "0.3.
|
|
75
|
-
"@vmz/vmz-win32-arm64": "0.3.
|
|
76
|
-
"@vmz/vmz-win32-x64": "0.3.
|
|
71
|
+
"@vmz/vmz-darwin-arm64": "0.3.11",
|
|
72
|
+
"@vmz/vmz-darwin-x64": "0.3.11",
|
|
73
|
+
"@vmz/vmz-linux-arm64": "0.3.11",
|
|
74
|
+
"@vmz/vmz-linux-x64": "0.3.11",
|
|
75
|
+
"@vmz/vmz-win32-arm64": "0.3.11",
|
|
76
|
+
"@vmz/vmz-win32-x64": "0.3.11"
|
|
77
77
|
},
|
|
78
78
|
"publishConfig": {
|
|
79
79
|
"access": "public"
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
},
|
|
89
89
|
"type": "module",
|
|
90
90
|
"types": "./dist/faces/index.d.ts",
|
|
91
|
-
"version": "0.3.
|
|
91
|
+
"version": "0.3.11"
|
|
92
92
|
}
|