ilha 0.9.3 → 0.9.5
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/README.md +2 -0
- package/dist/index.js +265 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -420,6 +420,8 @@ const Card = ilha.render(
|
|
|
420
420
|
);
|
|
421
421
|
```
|
|
422
422
|
|
|
423
|
+
Props (including JSX `children` and function callbacks) stay on the **live slot map** during parent render/mount. `data-ilha-props` only carries JSON-safe scalars for hydration hints — never rely on it for children or handlers. Nested islands under layout shells (`defineLayout` / `wrapLayout`) use the same path as page-level composition.
|
|
424
|
+
|
|
423
425
|
**Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
|
|
424
426
|
|
|
425
427
|
```ts
|
package/dist/index.js
CHANGED
|
@@ -121,8 +121,96 @@ function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
|
|
|
121
121
|
}
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Props safe to embed in `data-ilha-props`.
|
|
126
|
+
* Functions cannot round-trip through JSON; children are owned by the live slot
|
|
127
|
+
* map (client) and/or the inlined SSR subtree — never depend on the attr for them.
|
|
128
|
+
* RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
|
|
129
|
+
*/
|
|
130
|
+
function slotPropsForAttr(props) {
|
|
131
|
+
if (props === void 0) return void 0;
|
|
132
|
+
let out;
|
|
133
|
+
for (const key of Object.keys(props)) {
|
|
134
|
+
if (key === "children") continue;
|
|
135
|
+
const value = props[key];
|
|
136
|
+
if (typeof value === "function" || typeof value === "symbol") continue;
|
|
137
|
+
const encoded = encodeSlotPropValue(value);
|
|
138
|
+
if (encoded === void 0 && value !== void 0 && value !== null) continue;
|
|
139
|
+
(out ??= {})[key] = encoded;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
const SLOT_RAW = Symbol.for("ilha.raw");
|
|
144
|
+
function isRawHtmlValue(v) {
|
|
145
|
+
return !!(v && typeof v === "object" && SLOT_RAW in v);
|
|
146
|
+
}
|
|
147
|
+
function encodeSlotPropValue(value, seen) {
|
|
148
|
+
if (value == null) return value;
|
|
149
|
+
if (typeof value === "function" || typeof value === "symbol") return void 0;
|
|
150
|
+
if (typeof value !== "object") return value;
|
|
151
|
+
if (isRawHtmlValue(value)) return {
|
|
152
|
+
__ilha: "raw",
|
|
153
|
+
value: value.value
|
|
154
|
+
};
|
|
155
|
+
const visited = seen ?? /* @__PURE__ */ new WeakSet();
|
|
156
|
+
if (visited.has(value)) throw new TypeError("encodeSlotPropValue: circular reference in slot props");
|
|
157
|
+
visited.add(value);
|
|
158
|
+
if (Array.isArray(value)) return value.map((item) => encodeSlotPropValue(item, visited)).filter((item) => item !== void 0);
|
|
159
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
|
|
160
|
+
const obj = value;
|
|
161
|
+
const encoded = {};
|
|
162
|
+
for (const key of Object.keys(obj)) {
|
|
163
|
+
const next = encodeSlotPropValue(obj[key], visited);
|
|
164
|
+
if (next !== void 0 || obj[key] === null) encoded[key] = next;
|
|
165
|
+
}
|
|
166
|
+
return encoded;
|
|
167
|
+
}
|
|
168
|
+
function makeRawHtml(value) {
|
|
169
|
+
return {
|
|
170
|
+
[SLOT_RAW]: true,
|
|
171
|
+
value
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function reviveSlotPropValue(value) {
|
|
175
|
+
if (value == null || typeof value !== "object") return value;
|
|
176
|
+
if (Array.isArray(value)) return value.map(reviveSlotPropValue);
|
|
177
|
+
const obj = value;
|
|
178
|
+
if (obj.__ilha === "raw" && typeof obj.value === "string") return makeRawHtml(obj.value);
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const key of Object.keys(obj)) out[key] = reviveSlotPropValue(obj[key]);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Exact legacy shape produced when RawHtml lost its brand under JSON.stringify:
|
|
185
|
+
* a plain object with a single own key `value` that is a string.
|
|
186
|
+
*/
|
|
187
|
+
function isLegacyRawHtmlBlob(value) {
|
|
188
|
+
if (!value || typeof value !== "object" || isRawHtmlValue(value)) return false;
|
|
189
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return false;
|
|
190
|
+
const keys = Object.keys(value);
|
|
191
|
+
return keys.length === 1 && keys[0] === "value" && typeof value.value === "string";
|
|
192
|
+
}
|
|
193
|
+
/** Revive attr-parsed props (RawHtml tags). Children stay as plain data if present. */
|
|
194
|
+
function reviveSlotProps(props) {
|
|
195
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) return {};
|
|
196
|
+
const out = reviveSlotPropValue(props);
|
|
197
|
+
if ("children" in out) out.children = reviveLegacyChildren(out.children);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function reviveLegacyChildren(children) {
|
|
201
|
+
if (!Array.isArray(children)) {
|
|
202
|
+
if (isLegacyRawHtmlBlob(children)) return makeRawHtml(children.value);
|
|
203
|
+
return reviveSlotPropValue(children);
|
|
204
|
+
}
|
|
205
|
+
return children.map((child) => {
|
|
206
|
+
if (isLegacyRawHtmlBlob(child)) return makeRawHtml(child.value);
|
|
207
|
+
return reviveSlotPropValue(child);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** Serialized form of slot props, matching the decoded `data-ilha-props` attr. */
|
|
124
211
|
function serializeSlotProps(props) {
|
|
125
|
-
|
|
212
|
+
const safe = slotPropsForAttr(props);
|
|
213
|
+
return safe === void 0 ? "" : JSON.stringify(safe);
|
|
126
214
|
}
|
|
127
215
|
const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
|
|
128
216
|
const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
|
|
@@ -138,7 +226,19 @@ const MORPH_CONTROLLER_ATTRS = /* @__PURE__ */ new Set([
|
|
|
138
226
|
"data-selected",
|
|
139
227
|
"data-panel-open"
|
|
140
228
|
]);
|
|
229
|
+
/**
|
|
230
|
+
* Opt-in contract for controller-owned attributes: an element carrying
|
|
231
|
+
* `data-morph-preserve="attr-a attr-b class"` keeps those attributes exactly
|
|
232
|
+
* as they are in the live DOM — the morph neither overwrites nor removes them.
|
|
233
|
+
* The Areia list above stays as a built-in default for `[data-slot]` elements.
|
|
234
|
+
*/
|
|
235
|
+
const MORPH_PRESERVE_ATTR = "data-morph-preserve";
|
|
141
236
|
function shouldPreserveMorphAttr(el, name) {
|
|
237
|
+
if (name === MORPH_PRESERVE_ATTR) return el.hasAttribute(MORPH_PRESERVE_ATTR);
|
|
238
|
+
const custom = el.getAttribute(MORPH_PRESERVE_ATTR);
|
|
239
|
+
if (custom !== null) {
|
|
240
|
+
for (const token of custom.split(/\s+/)) if (token === name) return true;
|
|
241
|
+
}
|
|
142
242
|
return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
|
|
143
243
|
}
|
|
144
244
|
function syncAttributes(from, to) {
|
|
@@ -151,18 +251,30 @@ function syncAttributes(from, to) {
|
|
|
151
251
|
if (!to.hasAttribute(name)) from.removeAttribute(name);
|
|
152
252
|
}
|
|
153
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Morph identity key: explicit `data-key`, or the child-island slot id.
|
|
256
|
+
* Slot hosts are matched by id like keyed list items so a mounted child
|
|
257
|
+
* island's host element is reconciled IN PLACE on parent re-renders — never
|
|
258
|
+
* detached and reinserted, which would blur a focused element inside it.
|
|
259
|
+
*/
|
|
260
|
+
function morphKeyOf(el) {
|
|
261
|
+
const k = el.getAttribute("data-key");
|
|
262
|
+
if (k !== null) return `k:${k}`;
|
|
263
|
+
const s = el.getAttribute(SLOT_ATTR);
|
|
264
|
+
return s === null ? null : `s:${s}`;
|
|
265
|
+
}
|
|
154
266
|
function morphChildren(fromParent, toParent) {
|
|
155
267
|
const toNodes = Array.from(toParent.childNodes);
|
|
156
268
|
let fromKeyed = null;
|
|
157
269
|
for (const child of fromParent.children) {
|
|
158
|
-
const k = child
|
|
270
|
+
const k = morphKeyOf(child);
|
|
159
271
|
if (k !== null && !(fromKeyed ??= /* @__PURE__ */ new Map()).has(k)) fromKeyed.set(k, child);
|
|
160
272
|
}
|
|
161
273
|
let toKeys = null;
|
|
162
274
|
if (fromKeyed !== null) {
|
|
163
275
|
toKeys = /* @__PURE__ */ new Set();
|
|
164
276
|
for (const child of toParent.children) {
|
|
165
|
-
const k = child
|
|
277
|
+
const k = morphKeyOf(child);
|
|
166
278
|
if (k !== null) toKeys.add(k);
|
|
167
279
|
}
|
|
168
280
|
}
|
|
@@ -170,7 +282,7 @@ function morphChildren(fromParent, toParent) {
|
|
|
170
282
|
const toNode = toNodes[i];
|
|
171
283
|
let fromNode = fromParent.childNodes[i];
|
|
172
284
|
if (fromKeyed !== null) {
|
|
173
|
-
const toKey = toNode.nodeType === 1 ? toNode
|
|
285
|
+
const toKey = toNode.nodeType === 1 ? morphKeyOf(toNode) : null;
|
|
174
286
|
if (toKey !== null) {
|
|
175
287
|
const match = fromKeyed.get(toKey);
|
|
176
288
|
if (match) {
|
|
@@ -182,7 +294,7 @@ function morphChildren(fromParent, toParent) {
|
|
|
182
294
|
}
|
|
183
295
|
}
|
|
184
296
|
if (fromNode instanceof Element) {
|
|
185
|
-
const fromKey = fromNode
|
|
297
|
+
const fromKey = morphKeyOf(fromNode);
|
|
186
298
|
if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
|
|
187
299
|
fromParent.insertBefore(toNode.cloneNode(true), fromNode);
|
|
188
300
|
continue;
|
|
@@ -208,6 +320,18 @@ function morphChildren(fromParent, toParent) {
|
|
|
208
320
|
fromParent.replaceChild(toEl.cloneNode(true), fromEl);
|
|
209
321
|
continue;
|
|
210
322
|
}
|
|
323
|
+
if (fromEl.hasAttribute(LEAVING_ATTR)) {
|
|
324
|
+
fromParent.replaceChild(toEl.cloneNode(true), fromEl);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
{
|
|
328
|
+
const slotId = toEl.getAttribute(SLOT_ATTR);
|
|
329
|
+
if (slotId !== null && fromEl.getAttribute(SLOT_ATTR) === slotId) {
|
|
330
|
+
const props = toEl.getAttribute(PROPS_ATTR);
|
|
331
|
+
if (props !== null && fromEl.getAttribute(PROPS_ATTR) !== props) fromEl.setAttribute(PROPS_ATTR, props);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
211
335
|
if (fromEl.localName === "input" && fromEl.type !== toEl.type) {
|
|
212
336
|
fromParent.replaceChild(toEl.cloneNode(true), fromEl);
|
|
213
337
|
continue;
|
|
@@ -222,6 +346,22 @@ function morphChildren(fromParent, toParent) {
|
|
|
222
346
|
if (newValue !== hadValue) fromEl.value = newValue ?? "";
|
|
223
347
|
continue;
|
|
224
348
|
}
|
|
349
|
+
if (fromEl.localName === "select") {
|
|
350
|
+
const before = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const o of fromEl.options) before.set(o, {
|
|
352
|
+
attr: o.hasAttribute("selected"),
|
|
353
|
+
live: o.selected
|
|
354
|
+
});
|
|
355
|
+
syncAttributes(fromEl, toEl);
|
|
356
|
+
morphChildren(fromEl, toEl);
|
|
357
|
+
const options = Array.from(fromEl.options);
|
|
358
|
+
if (options.some((o) => o.hasAttribute("selected") !== (before.get(o)?.attr ?? false))) for (const o of options) o.selected = o.hasAttribute("selected");
|
|
359
|
+
else for (const o of options) {
|
|
360
|
+
const prev = before.get(o);
|
|
361
|
+
if (prev && o.selected !== prev.live) o.selected = prev.live;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
225
365
|
syncAttributes(fromEl, toEl);
|
|
226
366
|
if (fromEl.localName === "textarea") {
|
|
227
367
|
const newText = toEl.textContent ?? "";
|
|
@@ -234,9 +374,62 @@ function morphChildren(fromParent, toParent) {
|
|
|
234
374
|
}
|
|
235
375
|
while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild.remove();
|
|
236
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Invariant: the morph must never lose focus for an element that survives it.
|
|
379
|
+
* Surviving slot hosts and keyed elements are patched in place, but a genuine
|
|
380
|
+
* reorder still detaches-and-reinserts an ancestor of `document.activeElement`,
|
|
381
|
+
* which blurs it in real engines — snapshot before, restore after.
|
|
382
|
+
*/
|
|
383
|
+
function snapshotFocusForMorph() {
|
|
384
|
+
if (typeof document === "undefined") return null;
|
|
385
|
+
const active = document.activeElement;
|
|
386
|
+
if (!(active instanceof HTMLElement) || active === document.body) return null;
|
|
387
|
+
let selection = null;
|
|
388
|
+
let range = null;
|
|
389
|
+
try {
|
|
390
|
+
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) selection = {
|
|
391
|
+
start: active.selectionStart,
|
|
392
|
+
end: active.selectionEnd,
|
|
393
|
+
dir: active.selectionDirection
|
|
394
|
+
};
|
|
395
|
+
else if (active.isContentEditable) {
|
|
396
|
+
const sel = window.getSelection();
|
|
397
|
+
if (sel && sel.rangeCount > 0) range = sel.getRangeAt(0).cloneRange();
|
|
398
|
+
}
|
|
399
|
+
} catch {}
|
|
400
|
+
return {
|
|
401
|
+
active,
|
|
402
|
+
selection,
|
|
403
|
+
range
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function restoreFocusAfterMorph(snapshot) {
|
|
407
|
+
if (!snapshot) return;
|
|
408
|
+
const { active, selection, range } = snapshot;
|
|
409
|
+
if (!active.isConnected) return;
|
|
410
|
+
try {
|
|
411
|
+
if (document.activeElement !== active) active.focus({ preventScroll: true });
|
|
412
|
+
if (selection && selection.start !== null) {
|
|
413
|
+
const el = active;
|
|
414
|
+
if (el.selectionStart !== selection.start || el.selectionEnd !== selection.end) el.setSelectionRange(selection.start, selection.end, selection.dir ?? "none");
|
|
415
|
+
}
|
|
416
|
+
if (range && range.startContainer.isConnected) {
|
|
417
|
+
const sel = window.getSelection();
|
|
418
|
+
if (sel) {
|
|
419
|
+
const current = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
420
|
+
if (!(current !== null && current.startContainer === range.startContainer && current.startOffset === range.startOffset && current.endContainer === range.endContainer && current.endOffset === range.endOffset)) {
|
|
421
|
+
sel.removeAllRanges();
|
|
422
|
+
sel.addRange(range);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch {}
|
|
427
|
+
}
|
|
237
428
|
function morphInner(from, to) {
|
|
238
429
|
if (from.localName !== to.localName || from.namespaceURI !== to.namespaceURI) throw new Error("[ilha] morph: elements must match");
|
|
430
|
+
const focus = snapshotFocusForMorph();
|
|
239
431
|
morphChildren(from, to);
|
|
432
|
+
restoreFocusAfterMorph(focus);
|
|
240
433
|
}
|
|
241
434
|
function isStandardSchema(value) {
|
|
242
435
|
if (value == null || typeof value !== "object") return false;
|
|
@@ -283,13 +476,22 @@ const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
|
|
|
283
476
|
* on unmount. Not part of the public surface. */
|
|
284
477
|
const ISLAND_MOUNT_HANDLES = /* @__PURE__ */ new WeakMap();
|
|
285
478
|
const SLOT_ATTR = "data-ilha-slot";
|
|
479
|
+
/** Marks a slot host whose child island is mid leave-transition: it has been
|
|
480
|
+
* unmounted (and its slot id stripped) but stays connected so the leave
|
|
481
|
+
* animation can paint. The morph replaces such elements instead of matching
|
|
482
|
+
* or mutating them. */
|
|
483
|
+
const LEAVING_ATTR = "data-ilha-leaving";
|
|
286
484
|
const PROPS_ATTR = "data-ilha-props";
|
|
287
485
|
const STATE_ATTR = "data-ilha-state";
|
|
288
486
|
const CSS_ATTR = "data-ilha-css";
|
|
289
487
|
function buildScopedStyle(css) {
|
|
290
488
|
return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css.replace(/<\/style/gi, "<\\/style")}}</style>`;
|
|
291
489
|
}
|
|
292
|
-
const
|
|
490
|
+
const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
|
|
491
|
+
function renderCtxStack() {
|
|
492
|
+
const g = globalThis;
|
|
493
|
+
return g[RENDER_CTX_STACK] ??= [];
|
|
494
|
+
}
|
|
293
495
|
function pushRenderCtx(liveHost, asyncChildren) {
|
|
294
496
|
const ctx = {
|
|
295
497
|
slots: /* @__PURE__ */ new Map(),
|
|
@@ -298,14 +500,15 @@ function pushRenderCtx(liveHost, asyncChildren) {
|
|
|
298
500
|
pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
|
|
299
501
|
binds: []
|
|
300
502
|
};
|
|
301
|
-
renderCtxStack.push(ctx);
|
|
503
|
+
renderCtxStack().push(ctx);
|
|
302
504
|
return ctx;
|
|
303
505
|
}
|
|
304
506
|
function popRenderCtx() {
|
|
305
|
-
renderCtxStack.pop();
|
|
507
|
+
renderCtxStack().pop();
|
|
306
508
|
}
|
|
307
509
|
function currentRenderCtx() {
|
|
308
|
-
|
|
510
|
+
const stack = renderCtxStack();
|
|
511
|
+
return stack[stack.length - 1];
|
|
309
512
|
}
|
|
310
513
|
function isIsland(v) {
|
|
311
514
|
return typeof v === "function" && ISLAND in v;
|
|
@@ -325,7 +528,8 @@ function emitIslandSlot(island, props, key) {
|
|
|
325
528
|
props
|
|
326
529
|
});
|
|
327
530
|
const slotTag = getIslandSlotTag(island);
|
|
328
|
-
const
|
|
531
|
+
const attrProps = slotPropsForAttr(props);
|
|
532
|
+
const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
|
|
329
533
|
if (ctx?.liveHost) return wrapIslandSlotHtml(slotTag, id, propsAttr, "");
|
|
330
534
|
if (ctx?.pending) {
|
|
331
535
|
popRenderCtx();
|
|
@@ -337,7 +541,7 @@ function emitIslandSlot(island, props, key) {
|
|
|
337
541
|
}
|
|
338
542
|
return wrapIslandSlotHtml(slotTag, id, propsAttr, String(result));
|
|
339
543
|
} finally {
|
|
340
|
-
renderCtxStack.push(ctx);
|
|
544
|
+
renderCtxStack().push(ctx);
|
|
341
545
|
}
|
|
342
546
|
}
|
|
343
547
|
const inner = island.toString(props);
|
|
@@ -1428,7 +1632,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1428
1632
|
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
1429
1633
|
if (rawProps) {
|
|
1430
1634
|
const parsed = safeParseSnapshot(rawProps, PROPS_ATTR);
|
|
1431
|
-
if (parsed !== void 0) props = parsed;
|
|
1635
|
+
if (parsed !== void 0) props = reviveSlotProps(parsed);
|
|
1432
1636
|
}
|
|
1433
1637
|
}
|
|
1434
1638
|
const inputSignal = signal(resolveInput(props));
|
|
@@ -1521,6 +1725,8 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1521
1725
|
leavingSlots.delete(id);
|
|
1522
1726
|
};
|
|
1523
1727
|
if (result instanceof Promise) {
|
|
1728
|
+
entry.el.removeAttribute(SLOT_ATTR);
|
|
1729
|
+
entry.el.setAttribute(LEAVING_ATTR, "");
|
|
1524
1730
|
leavingSlots.set(id, {
|
|
1525
1731
|
el: entry.el,
|
|
1526
1732
|
token
|
|
@@ -1548,6 +1754,13 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1548
1754
|
}
|
|
1549
1755
|
return index;
|
|
1550
1756
|
}
|
|
1757
|
+
/** Push live props from a fresh slot map into already-mounted children. */
|
|
1758
|
+
function pushUpdatedProps(nextSlots) {
|
|
1759
|
+
for (const [id, entry] of mountedSlots) {
|
|
1760
|
+
const next = nextSlots.get(id);
|
|
1761
|
+
if (next) entry.updateProps(next.props);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1551
1764
|
function mountSlots(slotMap) {
|
|
1552
1765
|
for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
|
|
1553
1766
|
for (const [id, entry] of mountedSlots) {
|
|
@@ -1569,7 +1782,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1569
1782
|
const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
|
|
1570
1783
|
if (rawProps) {
|
|
1571
1784
|
const parsed = safeParseSnapshot(rawProps, `props on [${SLOT_ATTR}="${id}"]`);
|
|
1572
|
-
if (parsed !== void 0) slotProps = parsed;
|
|
1785
|
+
if (parsed !== void 0) slotProps = reviveSlotProps(parsed);
|
|
1573
1786
|
}
|
|
1574
1787
|
}
|
|
1575
1788
|
const prevSub = setActiveSub(void 0);
|
|
@@ -1747,6 +1960,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1747
1960
|
cleanups.push(stopDerived);
|
|
1748
1961
|
let initialized = false;
|
|
1749
1962
|
const initialRenderedHtml = initial.html;
|
|
1963
|
+
let lastRendered = null;
|
|
1750
1964
|
let renderEpoch = 0;
|
|
1751
1965
|
const stopRender = effect(() => {
|
|
1752
1966
|
const epoch = ++renderEpoch;
|
|
@@ -1759,14 +1973,24 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1759
1973
|
if (!initialized) {
|
|
1760
1974
|
initialized = true;
|
|
1761
1975
|
if (preserveSSRDom) {
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1976
|
+
pushUpdatedProps(newSlotMap);
|
|
1977
|
+
if (rendered === initialRenderedHtml) {
|
|
1978
|
+
lastRendered = rendered;
|
|
1979
|
+
return;
|
|
1765
1980
|
}
|
|
1766
|
-
if (rendered === initialRenderedHtml) return;
|
|
1767
1981
|
}
|
|
1768
|
-
if (rendered === initialRenderedHtml)
|
|
1769
|
-
|
|
1982
|
+
if (rendered === initialRenderedHtml) {
|
|
1983
|
+
lastRendered = rendered;
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) {
|
|
1987
|
+
pushUpdatedProps(newSlotMap);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (rendered === lastRendered && mountedSlots.size === newSlotMap.size && [...newSlotMap].every(([id, next]) => mountedSlots.get(id)?.island === next.island)) {
|
|
1992
|
+
pushUpdatedProps(newSlotMap);
|
|
1993
|
+
return;
|
|
1770
1994
|
}
|
|
1771
1995
|
detachListeners();
|
|
1772
1996
|
stopBindings();
|
|
@@ -1785,34 +2009,33 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
1785
2009
|
}
|
|
1786
2010
|
const applyMorph = () => {
|
|
1787
2011
|
if (epoch !== renderEpoch) return;
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
continue;
|
|
2012
|
+
if (newSlotMap.size < prevMountedCount) {
|
|
2013
|
+
let divergeFrom = Infinity;
|
|
2014
|
+
for (const [id, entry] of mountedSlots) {
|
|
2015
|
+
if (!id.startsWith("p:")) continue;
|
|
2016
|
+
const next = newSlotMap.get(id);
|
|
2017
|
+
if (!next) continue;
|
|
2018
|
+
const index = Number(id.slice(2));
|
|
2019
|
+
if (Number.isNaN(index) || index >= divergeFrom) continue;
|
|
2020
|
+
if (serializeSlotProps(next.props) !== (entry.el.getAttribute(PROPS_ATTR) ?? "")) divergeFrom = index;
|
|
2021
|
+
}
|
|
2022
|
+
for (const [id, entry] of mountedSlots) {
|
|
2023
|
+
if (!newSlotMap.has(id)) continue;
|
|
2024
|
+
if (!entry.el.isConnected) continue;
|
|
2025
|
+
if (id.startsWith("p:") && Number(id.slice(2)) >= divergeFrom) teardownMountedSlot(id, entry);
|
|
1796
2026
|
}
|
|
1797
|
-
const stubTag = entry.el.tagName.toLowerCase();
|
|
1798
|
-
const stub = document.createElement(stubTag);
|
|
1799
|
-
stub.setAttribute(SLOT_ATTR, id);
|
|
1800
|
-
const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
|
|
1801
|
-
if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
|
|
1802
|
-
entry.el.replaceWith(stub);
|
|
1803
|
-
preserved.set(id, entry.el);
|
|
1804
2027
|
}
|
|
1805
2028
|
const tpl = document.createElement("template");
|
|
1806
2029
|
const morphRootTag = host.tagName.toLowerCase();
|
|
1807
2030
|
tpl.innerHTML = `<${morphRootTag}>${html}</${morphRootTag}>`;
|
|
1808
2031
|
morphInner(host, tpl.content.firstElementChild);
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2032
|
+
lastRendered = rendered;
|
|
2033
|
+
let rehomeIndex = null;
|
|
2034
|
+
for (const [id, entry] of mountedSlots) {
|
|
2035
|
+
if (!newSlotMap.has(id) || entry.el.isConnected) continue;
|
|
2036
|
+
rehomeIndex ??= buildSlotIndex();
|
|
2037
|
+
const stub = rehomeIndex.get(id);
|
|
2038
|
+
if (stub && stub !== entry.el) stub.replaceWith(entry.el);
|
|
1816
2039
|
}
|
|
1817
2040
|
attachListeners();
|
|
1818
2041
|
stopBindings = applyTemplateBindings(host, newBinds);
|
|
@@ -2072,7 +2295,7 @@ function mountAll(registry, options = {}) {
|
|
|
2072
2295
|
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
2073
2296
|
if (rawProps) {
|
|
2074
2297
|
const parsed = safeParseSnapshot(rawProps, `${PROPS_ATTR} on [data-ilha="${name}"]`);
|
|
2075
|
-
if (parsed !== void 0) props = parsed;
|
|
2298
|
+
if (parsed !== void 0) props = reviveSlotProps(parsed);
|
|
2076
2299
|
}
|
|
2077
2300
|
unmounts.push(island.mount(host, props));
|
|
2078
2301
|
}
|