@vmz/core 0.3.5 → 0.3.7

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.
@@ -5,6 +5,18 @@ export declare function findOwnedDefaultSlot(root: any): any;
5
5
  export declare function findOwnedDefaultSlotTarget(root: any): any;
6
6
  export declare function createInstance(Component: any, props?: any): any;
7
7
  export declare function destroy(inst: any): void;
8
+ export declare function insertProjectedSlotNode(target: any, node: any): any;
9
+ /**
10
+ * Hold projected children until the component's default slot exists.
11
+ *
12
+ * A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
13
+ * skeleton) is absent at projection time. Appending to the component root would
14
+ * paint the child into the live page, so keep it detached and splice it in when
15
+ * the slot is mounted.
16
+ */
17
+ export declare function parkSlotNode(hostEl: any, node: any): void;
18
+ /** Move parked children into the now-mounted default slot. Returns true when drained. */
19
+ export declare function flushParkedSlotNodes(hostEl: any): boolean;
8
20
  export declare function disposeDomTree(root: any): void;
9
21
  export declare function snapshotInstanceState(inst: any): {};
10
22
  export declare function applyPreservedState(inst: any, state: any): void;
@@ -183,6 +183,87 @@ export function destroy(inst) {
183
183
  }
184
184
  }
185
185
  }
186
+ export function insertProjectedSlotNode(target, node) {
187
+ if (!target || !node)
188
+ return null;
189
+ if (String(target.tagName || '').toLowerCase() === 'slot') {
190
+ target.replaceWith(node);
191
+ return node;
192
+ }
193
+ target.appendChild(node);
194
+ return node;
195
+ }
196
+ /**
197
+ * Hold projected children until the component's default slot exists.
198
+ *
199
+ * A slot inside an unmounted `v-if` branch (closed drawer/dialog, loading
200
+ * skeleton) is absent at projection time. Appending to the component root would
201
+ * paint the child into the live page, so keep it detached and splice it in when
202
+ * the slot is mounted.
203
+ */
204
+ export function parkSlotNode(hostEl, node) {
205
+ if (!hostEl || !node)
206
+ return;
207
+ if (typeof MutationObserver === 'undefined') {
208
+ // Non-browser DOM (unit hosts): preserve the legacy root fallback.
209
+ const child = hostEl.__vmzInst;
210
+ const root = (child && child.__vmzDomRoot) || hostEl;
211
+ if (root && root.nodeType === 1 && typeof root.appendChild === 'function')
212
+ root.appendChild(node);
213
+ else if (typeof hostEl.appendChild === 'function')
214
+ hostEl.appendChild(node);
215
+ return;
216
+ }
217
+ const parked = hostEl.__vmzSlotParked || (hostEl.__vmzSlotParked = []);
218
+ parked.push(node);
219
+ if (hostEl.__vmzSlotObserver)
220
+ return;
221
+ hostEl.__vmzSlotObserver = new MutationObserver(() => {
222
+ flushParkedSlotNodes(hostEl);
223
+ });
224
+ const scope = (hostEl.__vmzInst && hostEl.__vmzInst.__vmzDomRoot) || hostEl;
225
+ if (scope && scope.nodeType === 1) {
226
+ hostEl.__vmzSlotObserver.observe(scope, { childList: true, subtree: true });
227
+ }
228
+ }
229
+ /** Move parked children into the now-mounted default slot. Returns true when drained. */
230
+ export function flushParkedSlotNodes(hostEl) {
231
+ if (!hostEl)
232
+ return false;
233
+ const parked = hostEl.__vmzSlotParked;
234
+ if (!parked || !parked.length)
235
+ return false;
236
+ const child = hostEl.__vmzInst;
237
+ const root = (child && child.__vmzDomRoot) || hostEl;
238
+ if (!root || root.nodeType !== 1)
239
+ return false;
240
+ const target = findOwnedDefaultSlotTarget(root);
241
+ if (!target || !target.parentNode)
242
+ return false;
243
+ if (hostEl.__vmzSlotObserver) {
244
+ hostEl.__vmzSlotObserver.disconnect();
245
+ hostEl.__vmzSlotObserver = null;
246
+ }
247
+ hostEl.__vmzSlotParked = [];
248
+ const tag = String(target.tagName || '').toLowerCase();
249
+ if (tag === 'slot') {
250
+ const frag = typeof document !== 'undefined' ? document.createDocumentFragment() : null;
251
+ if (!frag) {
252
+ target.replaceWith(parked[0]);
253
+ hostEl.__vmzSlotAnchor = parked[parked.length - 1];
254
+ return true;
255
+ }
256
+ for (const node of parked)
257
+ frag.appendChild(node);
258
+ target.replaceWith(frag);
259
+ hostEl.__vmzSlotAnchor = parked[parked.length - 1];
260
+ return true;
261
+ }
262
+ for (const node of parked)
263
+ target.appendChild(node);
264
+ hostEl.__vmzSlotAnchor = parked[parked.length - 1];
265
+ return true;
266
+ }
186
267
  export function disposeDomTree(root) {
187
268
  if (!root)
188
269
  return;
@@ -205,6 +286,14 @@ export function disposeDomTree(root) {
205
286
  node.__vmzInst = null;
206
287
  destroy(child);
207
288
  }
289
+ if (node.__vmzSlotObserver) {
290
+ node.__vmzSlotObserver.disconnect();
291
+ node.__vmzSlotObserver = null;
292
+ }
293
+ if (node.__vmzSlotParked)
294
+ node.__vmzSlotParked = [];
295
+ if (node.__vmzSlotAnchor)
296
+ node.__vmzSlotAnchor = null;
208
297
  let child = node.firstChild;
209
298
  while (child) {
210
299
  const next = child.nextSibling;
@@ -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, runDirectCreate, stripFns } from './component.js';
5
+ import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, insertProjectedSlotNode, parkSlotNode, runDirectCreate, 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';
@@ -431,16 +431,20 @@ export const directApi = {
431
431
  root = hostEl;
432
432
  const target = root && root.nodeType === 1 ? findOwnedDefaultSlotTarget(root) : null;
433
433
  if (target && target.parentNode) {
434
- if (String(target.tagName || '').toLowerCase() === 'slot') {
435
- target.replaceWith(node);
436
- return;
437
- }
438
- target.appendChild(node);
434
+ hostEl.__vmzSlotAnchor = insertProjectedSlotNode(target, node);
435
+ return;
436
+ }
437
+ // The `<slot>` was already replaced by an earlier projected child, so this
438
+ // call appends after the last one instead of re-searching for a slot.
439
+ const anchor = hostEl.__vmzSlotAnchor;
440
+ if (anchor && anchor.parentNode) {
441
+ anchor.parentNode.insertBefore(node, anchor.nextSibling);
442
+ hostEl.__vmzSlotAnchor = node;
439
443
  return;
440
444
  }
441
- if (root && root.nodeType === 1 && typeof root.appendChild === 'function')
442
- root.appendChild(node);
443
- else
444
- hostEl.appendChild(node);
445
+ // Component slot may live inside an unmounted `v-if` branch (closed overlay,
446
+ // loading skeleton). Parking the child until the slot appears keeps it out of
447
+ // the component root, where it would otherwise leak into the rendered page.
448
+ parkSlotNode(hostEl, node);
445
449
  },
446
450
  };
@@ -53,6 +53,32 @@ export function scheduleRefresh(inst, notice) {
53
53
  }
54
54
  });
55
55
  }
56
+ const ARROW_SIGNATURE = /^(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z_$][\w$]*))\s*=>\s*([\s\S]*)$/;
57
+ const EVENT_PASSTHROUGH_CALL = /^this\.([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*;?$/;
58
+ /**
59
+ * Direct-bind hint for `(ev) => this.method(ev)` handlers only. The closure is
60
+ * replaced by `method.call(inst, ev)`, so the shape must forward exactly the
61
+ * event: a handler that passes its own literal arguments (`() => this.pick("dark")`)
62
+ * must keep running as a closure, otherwise the event would overwrite the argument.
63
+ */
64
+ function inferEventPassthroughMethod(src) {
65
+ const arrow = src.match(ARROW_SIGNATURE);
66
+ if (!arrow)
67
+ return null;
68
+ const params = (arrow[1] ?? arrow[2] ?? '')
69
+ .split(',')
70
+ .map((p) => p.trim())
71
+ .filter(Boolean);
72
+ if (params.length !== 1)
73
+ return null;
74
+ let body = arrow[3].trim();
75
+ if (body.startsWith('{') && body.endsWith('}'))
76
+ body = body.slice(1, -1).trim();
77
+ const call = body.match(EVENT_PASSTHROUGH_CALL);
78
+ if (!call || call[2].trim() !== params[0])
79
+ return null;
80
+ return call[1];
81
+ }
56
82
  export function inferHandlerMethod(handler) {
57
83
  if (typeof handler !== 'function')
58
84
  return null;
@@ -62,8 +88,7 @@ export function inferHandlerMethod(handler) {
62
88
  let name = null;
63
89
  try {
64
90
  const src = Function.prototype.toString.call(handler);
65
- const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(/);
66
- name = m ? m[1] : null;
91
+ name = inferEventPassthroughMethod(src);
67
92
  }
68
93
  catch {
69
94
  name = null;
@@ -36,6 +36,26 @@ export async function runDirectSerializeTreeWithMounts(Component, inst) {
36
36
  }
37
37
  throw new Error('vmz:dom SSR child onMount expansion exceeded 32 rounds');
38
38
  }
39
+ /** HTML void elements never take a closing tag; `</br>` is parsed as another `<br>`. */
40
+ const VOID_TAGS = new Set([
41
+ 'area',
42
+ 'base',
43
+ 'br',
44
+ 'col',
45
+ 'embed',
46
+ 'hr',
47
+ 'img',
48
+ 'input',
49
+ 'link',
50
+ 'meta',
51
+ 'param',
52
+ 'source',
53
+ 'track',
54
+ 'wbr',
55
+ ]);
56
+ function isVoidTag(tag) {
57
+ return VOID_TAGS.has(String(tag).toLowerCase());
58
+ }
39
59
  function serializeOpenTag(node) {
40
60
  const tag = node.tag || 'div';
41
61
  let attrs = '';
@@ -75,10 +95,10 @@ export function flattenSerializeNode(node) {
75
95
  return String(node.__rawHtml);
76
96
  const { open } = serializeOpenTag(node);
77
97
  if (node.__rawHtml != null) {
78
- return `${open}${String(node.__rawHtml)}</${tag}>`;
98
+ return isVoidTag(tag) ? `${open}${String(node.__rawHtml)}` : `${open}${String(node.__rawHtml)}</${tag}>`;
79
99
  }
80
100
  const inner = (node.children || []).map(flattenSerializeNode).join('');
81
- return `${open}${inner}</${tag}>`;
101
+ return isVoidTag(tag) ? `${open}${inner}` : `${open}${inner}</${tag}>`;
82
102
  }
83
103
  return '';
84
104
  }
@@ -128,7 +148,8 @@ export function* streamSerializeChunks(node) {
128
148
  for (const c of node.children || [])
129
149
  yield* streamSerializeChunks(c);
130
150
  }
131
- yield `</${tag}>`;
151
+ if (!isVoidTag(tag))
152
+ yield `</${tag}>`;
132
153
  }
133
154
  }
134
155
  function fillRowKernelHtml(rk, item, key) {
@@ -527,8 +548,9 @@ const serializeApi = {
527
548
  target.children.push(node);
528
549
  return;
529
550
  }
530
- if (root)
531
- root.appendChild(node);
551
+ // No default slot in this render: children belong to a branch that is not
552
+ // mounted (closed overlay, loading skeleton). Mirror the browser runtime and
553
+ // drop them instead of painting them into the component root.
532
554
  },
533
555
  setHtml(el, value) {
534
556
  if (!el || el.__kind !== 'el')
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.5",
72
- "@vmz/vmz-darwin-x64": "0.3.5",
73
- "@vmz/vmz-linux-arm64": "0.3.5",
74
- "@vmz/vmz-linux-x64": "0.3.5",
75
- "@vmz/vmz-win32-arm64": "0.3.5",
76
- "@vmz/vmz-win32-x64": "0.3.5"
71
+ "@vmz/vmz-darwin-arm64": "0.3.7",
72
+ "@vmz/vmz-darwin-x64": "0.3.7",
73
+ "@vmz/vmz-linux-arm64": "0.3.7",
74
+ "@vmz/vmz-linux-x64": "0.3.7",
75
+ "@vmz/vmz-win32-arm64": "0.3.7",
76
+ "@vmz/vmz-win32-x64": "0.3.7"
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.5"
91
+ "version": "0.3.7"
92
92
  }