@pyreon/runtime-dom 0.1.0
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/LICENSE +21 -0
- package/README.md +65 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +1909 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +1845 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +355 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/devtools.ts +304 -0
- package/src/hydrate.ts +385 -0
- package/src/hydration-debug.ts +39 -0
- package/src/index.ts +43 -0
- package/src/keep-alive.ts +71 -0
- package/src/mount.ts +367 -0
- package/src/nodes.ts +741 -0
- package/src/props.ts +328 -0
- package/src/template.ts +81 -0
- package/src/tests/coverage-gaps.test.ts +2488 -0
- package/src/tests/coverage.test.ts +1123 -0
- package/src/tests/mount.test.ts +3098 -0
- package/src/tests/setup.ts +3 -0
- package/src/transition-group.ts +264 -0
- package/src/transition.ts +184 -0
|
@@ -0,0 +1,1845 @@
|
|
|
1
|
+
import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, createRef, dispatchToErrorBoundary, h, onMount, onUnmount, propagateError, reportError, runWithHooks } from "@pyreon/core";
|
|
2
|
+
import { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
|
|
3
|
+
|
|
4
|
+
//#region src/hydration-debug.ts
|
|
5
|
+
/**
|
|
6
|
+
* Hydration mismatch warnings.
|
|
7
|
+
*
|
|
8
|
+
* Enabled automatically in development (NODE_ENV !== "production").
|
|
9
|
+
* Can be toggled manually for testing or verbose production debugging.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { enableHydrationWarnings } from "@pyreon/runtime-dom"
|
|
13
|
+
* enableHydrationWarnings()
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function enableHydrationWarnings() {
|
|
17
|
+
_enabled = true;
|
|
18
|
+
}
|
|
19
|
+
function disableHydrationWarnings() {
|
|
20
|
+
_enabled = false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Emit a hydration mismatch warning.
|
|
24
|
+
* @param type - Kind of mismatch
|
|
25
|
+
* @param expected - What the VNode expected
|
|
26
|
+
* @param actual - What the DOM had
|
|
27
|
+
* @param path - Human-readable path in the tree, e.g. "root > div > span"
|
|
28
|
+
*/
|
|
29
|
+
function warnHydrationMismatch(_type, _expected, _actual, _path) {
|
|
30
|
+
if (!_enabled) return;
|
|
31
|
+
console.warn(`[Pyreon] Hydration mismatch (${_type}): expected ${String(_expected)}, got ${String(_actual)} at ${_path}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/devtools.ts
|
|
36
|
+
|
|
37
|
+
function registerComponent(id, name, el, parentId) {
|
|
38
|
+
const entry = {
|
|
39
|
+
id,
|
|
40
|
+
name,
|
|
41
|
+
el,
|
|
42
|
+
parentId,
|
|
43
|
+
childIds: []
|
|
44
|
+
};
|
|
45
|
+
_components.set(id, entry);
|
|
46
|
+
if (parentId) {
|
|
47
|
+
const parent = _components.get(parentId);
|
|
48
|
+
if (parent) parent.childIds.push(id);
|
|
49
|
+
}
|
|
50
|
+
for (const cb of _mountListeners) cb(entry);
|
|
51
|
+
}
|
|
52
|
+
function unregisterComponent(id) {
|
|
53
|
+
const entry = _components.get(id);
|
|
54
|
+
if (!entry) return;
|
|
55
|
+
if (entry.parentId) {
|
|
56
|
+
const parent = _components.get(entry.parentId);
|
|
57
|
+
if (parent) parent.childIds = parent.childIds.filter(c => c !== id);
|
|
58
|
+
}
|
|
59
|
+
_components.delete(id);
|
|
60
|
+
for (const cb of _unmountListeners) cb(id);
|
|
61
|
+
}
|
|
62
|
+
function findComponentForElement(el) {
|
|
63
|
+
let node = el;
|
|
64
|
+
while (node) {
|
|
65
|
+
for (const entry of _components.values()) if (entry.el === node) return entry;
|
|
66
|
+
node = node.parentElement;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function createOverlayElements() {
|
|
71
|
+
if (_overlayEl) return;
|
|
72
|
+
_overlayEl = document.createElement("div");
|
|
73
|
+
_overlayEl.id = "__pyreon-overlay";
|
|
74
|
+
_overlayEl.style.cssText = "position:fixed;pointer-events:none;border:2px solid #00b4d8;border-radius:3px;z-index:999999;display:none;transition:all 0.08s ease-out;";
|
|
75
|
+
_tooltipEl = document.createElement("div");
|
|
76
|
+
_tooltipEl.style.cssText = "position:fixed;pointer-events:none;background:#1a1a2e;color:#e0e0e0;font:12px/1.4 ui-monospace,monospace;padding:6px 10px;border-radius:4px;z-index:999999;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.3);max-width:400px;white-space:pre-wrap;";
|
|
77
|
+
document.body.appendChild(_overlayEl);
|
|
78
|
+
document.body.appendChild(_tooltipEl);
|
|
79
|
+
}
|
|
80
|
+
function positionOverlay(rect) {
|
|
81
|
+
if (!_overlayEl) return;
|
|
82
|
+
_overlayEl.style.display = "block";
|
|
83
|
+
_overlayEl.style.top = `${rect.top}px`;
|
|
84
|
+
_overlayEl.style.left = `${rect.left}px`;
|
|
85
|
+
_overlayEl.style.width = `${rect.width}px`;
|
|
86
|
+
_overlayEl.style.height = `${rect.height}px`;
|
|
87
|
+
}
|
|
88
|
+
function positionTooltip(entry, rect) {
|
|
89
|
+
if (!_tooltipEl) return;
|
|
90
|
+
const childCount = entry.childIds.length;
|
|
91
|
+
let info = `<${entry.name}>`;
|
|
92
|
+
if (childCount > 0) info += `\n ${childCount} child component${childCount === 1 ? "" : "s"}`;
|
|
93
|
+
_tooltipEl.textContent = info;
|
|
94
|
+
_tooltipEl.style.display = "block";
|
|
95
|
+
_tooltipEl.style.top = `${rect.top - 30}px`;
|
|
96
|
+
_tooltipEl.style.left = `${rect.left}px`;
|
|
97
|
+
if (rect.top < 35) _tooltipEl.style.top = `${rect.bottom + 4}px`;
|
|
98
|
+
}
|
|
99
|
+
function hideOverlayElements() {
|
|
100
|
+
if (_overlayEl) _overlayEl.style.display = "none";
|
|
101
|
+
if (_tooltipEl) _tooltipEl.style.display = "none";
|
|
102
|
+
_currentHighlight = null;
|
|
103
|
+
}
|
|
104
|
+
/** @internal — exported for testing only */
|
|
105
|
+
function onOverlayMouseMove(e) {
|
|
106
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
107
|
+
if (!target || target === _overlayEl || target === _tooltipEl) return;
|
|
108
|
+
const entry = findComponentForElement(target);
|
|
109
|
+
if (!entry?.el) {
|
|
110
|
+
hideOverlayElements();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (entry.el === _currentHighlight) return;
|
|
114
|
+
_currentHighlight = entry.el;
|
|
115
|
+
const rect = entry.el.getBoundingClientRect();
|
|
116
|
+
positionOverlay(rect);
|
|
117
|
+
positionTooltip(entry, rect);
|
|
118
|
+
}
|
|
119
|
+
/** @internal — exported for testing only */
|
|
120
|
+
function onOverlayClick(e) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
124
|
+
if (!target) return;
|
|
125
|
+
const entry = findComponentForElement(target);
|
|
126
|
+
if (entry) {
|
|
127
|
+
console.group(`[Pyreon] <${entry.name}>`);
|
|
128
|
+
console.log("element:", entry.el);
|
|
129
|
+
console.log("children:", entry.childIds.length);
|
|
130
|
+
if (entry.parentId) {
|
|
131
|
+
const parent = _components.get(entry.parentId);
|
|
132
|
+
if (parent) console.log("parent:", `<${parent.name}>`);
|
|
133
|
+
}
|
|
134
|
+
console.groupEnd();
|
|
135
|
+
}
|
|
136
|
+
disableOverlay();
|
|
137
|
+
}
|
|
138
|
+
function onOverlayKeydown(e) {
|
|
139
|
+
if (e.key === "Escape") disableOverlay();
|
|
140
|
+
}
|
|
141
|
+
function enableOverlay() {
|
|
142
|
+
if (_overlayActive) return;
|
|
143
|
+
_overlayActive = true;
|
|
144
|
+
createOverlayElements();
|
|
145
|
+
document.addEventListener("mousemove", onOverlayMouseMove, true);
|
|
146
|
+
document.addEventListener("click", onOverlayClick, true);
|
|
147
|
+
document.addEventListener("keydown", onOverlayKeydown, true);
|
|
148
|
+
document.body.style.cursor = "crosshair";
|
|
149
|
+
}
|
|
150
|
+
function disableOverlay() {
|
|
151
|
+
if (!_overlayActive) return;
|
|
152
|
+
_overlayActive = false;
|
|
153
|
+
document.removeEventListener("mousemove", onOverlayMouseMove, true);
|
|
154
|
+
document.removeEventListener("click", onOverlayClick, true);
|
|
155
|
+
document.removeEventListener("keydown", onOverlayKeydown, true);
|
|
156
|
+
document.body.style.cursor = "";
|
|
157
|
+
if (_overlayEl) _overlayEl.style.display = "none";
|
|
158
|
+
if (_tooltipEl) _tooltipEl.style.display = "none";
|
|
159
|
+
_currentHighlight = null;
|
|
160
|
+
}
|
|
161
|
+
function installDevTools() {
|
|
162
|
+
if (!_hasWindow || _installed) return;
|
|
163
|
+
_installed = true;
|
|
164
|
+
const devtools = {
|
|
165
|
+
version: "0.1.0",
|
|
166
|
+
getComponentTree() {
|
|
167
|
+
return Array.from(_components.values()).filter(e => e.parentId === null);
|
|
168
|
+
},
|
|
169
|
+
getAllComponents() {
|
|
170
|
+
return Array.from(_components.values());
|
|
171
|
+
},
|
|
172
|
+
highlight(id) {
|
|
173
|
+
const entry = _components.get(id);
|
|
174
|
+
if (!entry?.el) return;
|
|
175
|
+
const el = entry.el;
|
|
176
|
+
const prev = el.style.outline;
|
|
177
|
+
el.style.outline = "2px solid #00b4d8";
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
el.style.outline = prev;
|
|
180
|
+
}, 1500);
|
|
181
|
+
},
|
|
182
|
+
onComponentMount(cb) {
|
|
183
|
+
_mountListeners.push(cb);
|
|
184
|
+
return () => {
|
|
185
|
+
const i = _mountListeners.indexOf(cb);
|
|
186
|
+
if (i >= 0) _mountListeners.splice(i, 1);
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
onComponentUnmount(cb) {
|
|
190
|
+
_unmountListeners.push(cb);
|
|
191
|
+
return () => {
|
|
192
|
+
const i = _unmountListeners.indexOf(cb);
|
|
193
|
+
if (i >= 0) _unmountListeners.splice(i, 1);
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
enableOverlay,
|
|
197
|
+
disableOverlay
|
|
198
|
+
};
|
|
199
|
+
window.__PYREON_DEVTOOLS__ = devtools;
|
|
200
|
+
window.addEventListener("keydown", e => {
|
|
201
|
+
if (e.ctrlKey && e.shiftKey && e.key === "P") {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
if (_overlayActive) disableOverlay();else enableOverlay();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const win = window;
|
|
207
|
+
win.$p = {
|
|
208
|
+
components: () => devtools.getAllComponents(),
|
|
209
|
+
tree: () => devtools.getComponentTree(),
|
|
210
|
+
highlight: id => devtools.highlight(id),
|
|
211
|
+
inspect: () => {
|
|
212
|
+
if (_overlayActive) disableOverlay();else enableOverlay();
|
|
213
|
+
},
|
|
214
|
+
stats: () => {
|
|
215
|
+
const all = devtools.getAllComponents();
|
|
216
|
+
const roots = devtools.getComponentTree();
|
|
217
|
+
console.log(`[Pyreon] ${all.length} component${all.length === 1 ? "" : "s"}, ${roots.length} root${roots.length === 1 ? "" : "s"}`);
|
|
218
|
+
return {
|
|
219
|
+
total: all.length,
|
|
220
|
+
roots: roots.length
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
help: () => {
|
|
224
|
+
console.log("[Pyreon] $p commands:\n $p.components() — list all mounted components\n $p.tree() — component tree (roots only)\n $p.highlight(id)— outline a component\n $p.inspect() — toggle component inspector\n $p.stats() — print component count\n $p.help() — this message");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/nodes.ts
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Move all nodes strictly between `start` and `end` into a throwaway
|
|
234
|
+
* DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
|
|
235
|
+
*
|
|
236
|
+
* This is dramatically faster than Range.deleteContents() in JS-based DOMs
|
|
237
|
+
* (happy-dom, jsdom) where deleting connected nodes with deep subtrees is O(n²).
|
|
238
|
+
* In real browsers both approaches are similar, but the fragment approach is
|
|
239
|
+
* never slower and avoids the pathological case.
|
|
240
|
+
*
|
|
241
|
+
* After this call every moved node has isConnected=false, so cleanup functions
|
|
242
|
+
* that guard removeChild with `isConnected !== false` become no-ops.
|
|
243
|
+
*/
|
|
244
|
+
function clearBetween(start, end) {
|
|
245
|
+
const frag = document.createDocumentFragment();
|
|
246
|
+
let cur = start.nextSibling;
|
|
247
|
+
while (cur && cur !== end) {
|
|
248
|
+
const next = cur.nextSibling;
|
|
249
|
+
frag.appendChild(cur);
|
|
250
|
+
cur = next;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Mount a reactive node whose content changes over time.
|
|
255
|
+
*
|
|
256
|
+
* A comment node is used as a stable anchor point in the DOM.
|
|
257
|
+
* On each change: old nodes are removed, new ones inserted before the anchor.
|
|
258
|
+
*/
|
|
259
|
+
function mountReactive(accessor, parent, anchor, mount) {
|
|
260
|
+
const marker = document.createComment("pyreon");
|
|
261
|
+
parent.insertBefore(marker, anchor);
|
|
262
|
+
let currentCleanup = () => {};
|
|
263
|
+
let generation = 0;
|
|
264
|
+
const e = effect(() => {
|
|
265
|
+
const myGen = ++generation;
|
|
266
|
+
runUntracked(() => currentCleanup());
|
|
267
|
+
currentCleanup = () => {};
|
|
268
|
+
const value = accessor();
|
|
269
|
+
if (__DEV__$4 && typeof value === "function") console.warn("[Pyreon] Reactive accessor returned a function instead of a value. Did you forget to call the signal?");
|
|
270
|
+
if (value != null && value !== false) {
|
|
271
|
+
const cleanup = mount(value, parent, marker);
|
|
272
|
+
if (myGen === generation) currentCleanup = cleanup;else cleanup();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return () => {
|
|
276
|
+
e.dispose();
|
|
277
|
+
currentCleanup();
|
|
278
|
+
marker.parentNode?.removeChild(marker);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function growLisArrays(lis, n) {
|
|
282
|
+
if (n <= lis.pred.length) return lis;
|
|
283
|
+
return {
|
|
284
|
+
tails: new Int32Array(n + 16),
|
|
285
|
+
tailIdx: new Int32Array(n + 16),
|
|
286
|
+
pred: new Int32Array(n + 16),
|
|
287
|
+
stay: new Uint8Array(n + 16)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function computeKeyedLis(lis, n, newKeyOrder, curPos) {
|
|
291
|
+
const {
|
|
292
|
+
tails,
|
|
293
|
+
tailIdx,
|
|
294
|
+
pred
|
|
295
|
+
} = lis;
|
|
296
|
+
let lisLen = 0;
|
|
297
|
+
for (let i = 0; i < n; i++) {
|
|
298
|
+
const key = newKeyOrder[i];
|
|
299
|
+
if (key === void 0) continue;
|
|
300
|
+
const v = curPos.get(key) ?? -1;
|
|
301
|
+
if (v < 0) continue;
|
|
302
|
+
let lo = 0;
|
|
303
|
+
let hi = lisLen;
|
|
304
|
+
while (lo < hi) {
|
|
305
|
+
const mid = lo + hi >> 1;
|
|
306
|
+
if (tails[mid] < v) lo = mid + 1;else hi = mid;
|
|
307
|
+
}
|
|
308
|
+
tails[lo] = v;
|
|
309
|
+
tailIdx[lo] = i;
|
|
310
|
+
if (lo > 0) pred[i] = tailIdx[lo - 1];
|
|
311
|
+
if (lo === lisLen) lisLen++;
|
|
312
|
+
}
|
|
313
|
+
return lisLen;
|
|
314
|
+
}
|
|
315
|
+
function markStayingEntries(lis, lisLen) {
|
|
316
|
+
const {
|
|
317
|
+
tailIdx,
|
|
318
|
+
pred,
|
|
319
|
+
stay
|
|
320
|
+
} = lis;
|
|
321
|
+
let cur = lisLen > 0 ? tailIdx[lisLen - 1] : -1;
|
|
322
|
+
while (cur !== -1) {
|
|
323
|
+
stay[cur] = 1;
|
|
324
|
+
cur = pred[cur];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function applyKeyedMoves(n, newKeyOrder, stay, cache, parent, tailMarker) {
|
|
328
|
+
let cursor = tailMarker;
|
|
329
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
330
|
+
const key = newKeyOrder[i];
|
|
331
|
+
if (key === void 0) continue;
|
|
332
|
+
const entry = cache.get(key);
|
|
333
|
+
if (!entry) continue;
|
|
334
|
+
if (!stay[i]) moveEntryBefore(parent, entry.anchor, cursor);
|
|
335
|
+
cursor = entry.anchor;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Grow LIS typed arrays if needed, then compute and apply reorder. */
|
|
339
|
+
function keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker) {
|
|
340
|
+
const grown = growLisArrays(lis, n);
|
|
341
|
+
grown.pred.fill(-1, 0, n);
|
|
342
|
+
grown.stay.fill(0, 0, n);
|
|
343
|
+
markStayingEntries(grown, computeKeyedLis(grown, n, newKeyOrder, curPos));
|
|
344
|
+
applyKeyedMoves(n, newKeyOrder, grown.stay, cache, parent, tailMarker);
|
|
345
|
+
return grown;
|
|
346
|
+
}
|
|
347
|
+
function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
|
|
348
|
+
const startMarker = document.createComment("");
|
|
349
|
+
const tailMarker = document.createComment("");
|
|
350
|
+
parent.insertBefore(startMarker, listAnchor);
|
|
351
|
+
parent.insertBefore(tailMarker, listAnchor);
|
|
352
|
+
const cache = /* @__PURE__ */new Map();
|
|
353
|
+
const curPos = /* @__PURE__ */new Map();
|
|
354
|
+
let currentKeyOrder = [];
|
|
355
|
+
let lis = {
|
|
356
|
+
tails: new Int32Array(16),
|
|
357
|
+
tailIdx: new Int32Array(16),
|
|
358
|
+
pred: new Int32Array(16),
|
|
359
|
+
stay: new Uint8Array(16)
|
|
360
|
+
};
|
|
361
|
+
const collectKeyOrder = newList => {
|
|
362
|
+
const newKeyOrder = [];
|
|
363
|
+
const newKeySet = /* @__PURE__ */new Set();
|
|
364
|
+
for (const vnode of newList) {
|
|
365
|
+
const key = vnode.key;
|
|
366
|
+
if (key !== null && key !== void 0) {
|
|
367
|
+
newKeyOrder.push(key);
|
|
368
|
+
newKeySet.add(key);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
newKeyOrder,
|
|
373
|
+
newKeySet
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
const removeStaleEntries = newKeySet => {
|
|
377
|
+
for (const [key, entry] of cache) {
|
|
378
|
+
if (newKeySet.has(key)) continue;
|
|
379
|
+
entry.cleanup();
|
|
380
|
+
entry.anchor.parentNode?.removeChild(entry.anchor);
|
|
381
|
+
cache.delete(key);
|
|
382
|
+
curPos.delete(key);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const mountNewEntries = newList => {
|
|
386
|
+
for (const vnode of newList) {
|
|
387
|
+
const key = vnode.key;
|
|
388
|
+
if (key === null || key === void 0) continue;
|
|
389
|
+
if (cache.has(key)) continue;
|
|
390
|
+
const anchor = document.createComment("");
|
|
391
|
+
_keyedAnchors.add(anchor);
|
|
392
|
+
parent.insertBefore(anchor, tailMarker);
|
|
393
|
+
const cleanup = mountVNode(vnode, parent, tailMarker);
|
|
394
|
+
cache.set(key, {
|
|
395
|
+
anchor,
|
|
396
|
+
cleanup
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const e = effect(() => {
|
|
401
|
+
const newList = accessor();
|
|
402
|
+
const n = newList.length;
|
|
403
|
+
if (n === 0 && cache.size > 0) {
|
|
404
|
+
for (const entry of cache.values()) entry.cleanup();
|
|
405
|
+
cache.clear();
|
|
406
|
+
curPos.clear();
|
|
407
|
+
currentKeyOrder = [];
|
|
408
|
+
clearBetween(startMarker, tailMarker);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const {
|
|
412
|
+
newKeyOrder,
|
|
413
|
+
newKeySet
|
|
414
|
+
} = collectKeyOrder(newList);
|
|
415
|
+
removeStaleEntries(newKeySet);
|
|
416
|
+
mountNewEntries(newList);
|
|
417
|
+
if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
|
|
418
|
+
curPos.clear();
|
|
419
|
+
for (let i = 0; i < newKeyOrder.length; i++) {
|
|
420
|
+
const k = newKeyOrder[i];
|
|
421
|
+
if (k !== void 0) curPos.set(k, i);
|
|
422
|
+
}
|
|
423
|
+
currentKeyOrder = newKeyOrder;
|
|
424
|
+
});
|
|
425
|
+
return () => {
|
|
426
|
+
e.dispose();
|
|
427
|
+
for (const entry of cache.values()) {
|
|
428
|
+
entry.cleanup();
|
|
429
|
+
entry.anchor.parentNode?.removeChild(entry.anchor);
|
|
430
|
+
}
|
|
431
|
+
cache.clear();
|
|
432
|
+
startMarker.parentNode?.removeChild(startMarker);
|
|
433
|
+
tailMarker.parentNode?.removeChild(tailMarker);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** Maximum number of displaced positions before falling back to full LIS. */
|
|
437
|
+
|
|
438
|
+
/** Try small-k reorder; returns true if handled, false if LIS fallback needed. */
|
|
439
|
+
function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker) {
|
|
440
|
+
if (n !== currentKeys.length) return false;
|
|
441
|
+
const diffs = [];
|
|
442
|
+
for (let i = 0; i < n; i++) if (newKeys[i] !== currentKeys[i]) {
|
|
443
|
+
diffs.push(i);
|
|
444
|
+
if (diffs.length > SMALL_K) return false;
|
|
445
|
+
}
|
|
446
|
+
if (diffs.length > 0) smallKPlace(liveParent, diffs, newKeys, cache, tailMarker);
|
|
447
|
+
for (const i of diffs) {
|
|
448
|
+
const cached = cache.get(newKeys[i]);
|
|
449
|
+
if (cached) cached.pos = i;
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
function computeForLis(lis, n, newKeys, cache) {
|
|
454
|
+
const {
|
|
455
|
+
tails,
|
|
456
|
+
tailIdx,
|
|
457
|
+
pred
|
|
458
|
+
} = lis;
|
|
459
|
+
let lisLen = 0;
|
|
460
|
+
for (let i = 0; i < n; i++) {
|
|
461
|
+
const key = newKeys[i];
|
|
462
|
+
const v = cache.get(key)?.pos ?? 0;
|
|
463
|
+
let lo = 0;
|
|
464
|
+
let hi = lisLen;
|
|
465
|
+
while (lo < hi) {
|
|
466
|
+
const mid = lo + hi >> 1;
|
|
467
|
+
if (tails[mid] < v) lo = mid + 1;else hi = mid;
|
|
468
|
+
}
|
|
469
|
+
tails[lo] = v;
|
|
470
|
+
tailIdx[lo] = i;
|
|
471
|
+
if (lo > 0) pred[i] = tailIdx[lo - 1];
|
|
472
|
+
if (lo === lisLen) lisLen++;
|
|
473
|
+
}
|
|
474
|
+
return lisLen;
|
|
475
|
+
}
|
|
476
|
+
function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
|
|
477
|
+
let cursor = tailMarker;
|
|
478
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
479
|
+
const entry = cache.get(newKeys[i]);
|
|
480
|
+
if (!entry) continue;
|
|
481
|
+
if (!stay[i]) moveEntryBefore(liveParent, entry.anchor, cursor);
|
|
482
|
+
cursor = entry.anchor;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** LIS-based reorder for mountFor. */
|
|
486
|
+
function forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker) {
|
|
487
|
+
const grown = growLisArrays(lis, n);
|
|
488
|
+
grown.pred.fill(-1, 0, n);
|
|
489
|
+
grown.stay.fill(0, 0, n);
|
|
490
|
+
markStayingEntries(grown, computeForLis(grown, n, newKeys, cache));
|
|
491
|
+
applyForMoves(n, newKeys, grown.stay, cache, liveParent, tailMarker);
|
|
492
|
+
for (let i = 0; i < n; i++) {
|
|
493
|
+
const cached = cache.get(newKeys[i]);
|
|
494
|
+
if (cached) cached.pos = i;
|
|
495
|
+
}
|
|
496
|
+
return grown;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Keyed reconciler that works directly on the source item array.
|
|
500
|
+
*
|
|
501
|
+
* Optimizations:
|
|
502
|
+
* - Calls renderItem() only for NEW keys — 0 VNode allocations for reorders
|
|
503
|
+
* - Small-k fast path: if <= SMALL_K positions changed, skips LIS
|
|
504
|
+
* - Fast clear path: moves nodes to DocumentFragment for O(n) bulk detach
|
|
505
|
+
* - Fresh render fast path: skips stale-check and reorder on first render
|
|
506
|
+
*/
|
|
507
|
+
function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
|
|
508
|
+
const startMarker = document.createComment("");
|
|
509
|
+
const tailMarker = document.createComment("");
|
|
510
|
+
parent.insertBefore(startMarker, anchor);
|
|
511
|
+
parent.insertBefore(tailMarker, anchor);
|
|
512
|
+
let cache = /* @__PURE__ */new Map();
|
|
513
|
+
let currentKeys = [];
|
|
514
|
+
let cleanupCount = 0;
|
|
515
|
+
let anchorsRegistered = false;
|
|
516
|
+
let lis = {
|
|
517
|
+
tails: new Int32Array(16),
|
|
518
|
+
tailIdx: new Int32Array(16),
|
|
519
|
+
pred: new Int32Array(16),
|
|
520
|
+
stay: new Uint8Array(16)
|
|
521
|
+
};
|
|
522
|
+
const warnDuplicateKeys = (seen, key) => {
|
|
523
|
+
if (!__DEV__$4 || !seen) return;
|
|
524
|
+
if (seen.has(key)) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
|
|
525
|
+
seen.add(key);
|
|
526
|
+
};
|
|
527
|
+
/** Render item into container, update cache+cleanupCount. No anchor registration. */
|
|
528
|
+
const renderInto = (item, key, pos, container, before) => {
|
|
529
|
+
const result = renderItem(item);
|
|
530
|
+
if (result.__isNative) {
|
|
531
|
+
const native = result;
|
|
532
|
+
container.insertBefore(native.el, before);
|
|
533
|
+
cache.set(key, {
|
|
534
|
+
anchor: native.el,
|
|
535
|
+
cleanup: native.cleanup,
|
|
536
|
+
pos
|
|
537
|
+
});
|
|
538
|
+
if (native.cleanup) cleanupCount++;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const priorLast = before ? before.previousSibling : container.lastChild;
|
|
542
|
+
const cl = mountChild(result, container, before);
|
|
543
|
+
const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild;
|
|
544
|
+
if (!firstMounted || firstMounted === before) {
|
|
545
|
+
const ph = document.createComment("");
|
|
546
|
+
container.insertBefore(ph, before);
|
|
547
|
+
cache.set(key, {
|
|
548
|
+
anchor: ph,
|
|
549
|
+
cleanup: cl,
|
|
550
|
+
pos
|
|
551
|
+
});
|
|
552
|
+
} else cache.set(key, {
|
|
553
|
+
anchor: firstMounted,
|
|
554
|
+
cleanup: cl,
|
|
555
|
+
pos
|
|
556
|
+
});
|
|
557
|
+
cleanupCount++;
|
|
558
|
+
};
|
|
559
|
+
const handleFreshRender = (items, n, liveParent) => {
|
|
560
|
+
const frag = document.createDocumentFragment();
|
|
561
|
+
const keys = new Array(n);
|
|
562
|
+
const _seenKeys = __DEV__$4 ? /* @__PURE__ */new Set() : null;
|
|
563
|
+
for (let i = 0; i < n; i++) {
|
|
564
|
+
const item = items[i];
|
|
565
|
+
const key = getKey(item);
|
|
566
|
+
warnDuplicateKeys(_seenKeys, key);
|
|
567
|
+
keys[i] = key;
|
|
568
|
+
renderInto(item, key, i, frag, null);
|
|
569
|
+
}
|
|
570
|
+
liveParent.insertBefore(frag, tailMarker);
|
|
571
|
+
anchorsRegistered = false;
|
|
572
|
+
currentKeys = keys;
|
|
573
|
+
};
|
|
574
|
+
const collectNewKeys = (items, n) => {
|
|
575
|
+
const newKeys = new Array(n);
|
|
576
|
+
const _seenUpdate = __DEV__$4 ? /* @__PURE__ */new Set() : null;
|
|
577
|
+
for (let i = 0; i < n; i++) {
|
|
578
|
+
newKeys[i] = getKey(items[i]);
|
|
579
|
+
warnDuplicateKeys(_seenUpdate, newKeys[i]);
|
|
580
|
+
}
|
|
581
|
+
return newKeys;
|
|
582
|
+
};
|
|
583
|
+
const handleReplaceAll = (items, n, newKeys, liveParent) => {
|
|
584
|
+
if (cleanupCount > 0) {
|
|
585
|
+
for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
|
|
586
|
+
}
|
|
587
|
+
cache = /* @__PURE__ */new Map();
|
|
588
|
+
cleanupCount = 0;
|
|
589
|
+
const parentParent = liveParent.parentNode;
|
|
590
|
+
const canSwap = parentParent && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker;
|
|
591
|
+
const frag = document.createDocumentFragment();
|
|
592
|
+
for (let i = 0; i < n; i++) renderInto(items[i], newKeys[i], i, frag, null);
|
|
593
|
+
anchorsRegistered = false;
|
|
594
|
+
if (canSwap) {
|
|
595
|
+
const fresh = liveParent.cloneNode(false);
|
|
596
|
+
fresh.appendChild(startMarker);
|
|
597
|
+
fresh.appendChild(frag);
|
|
598
|
+
fresh.appendChild(tailMarker);
|
|
599
|
+
parentParent.replaceChild(fresh, liveParent);
|
|
600
|
+
} else {
|
|
601
|
+
clearBetween(startMarker, tailMarker);
|
|
602
|
+
liveParent.insertBefore(frag, tailMarker);
|
|
603
|
+
}
|
|
604
|
+
currentKeys = newKeys;
|
|
605
|
+
};
|
|
606
|
+
const removeStaleForEntries = newKeySet => {
|
|
607
|
+
for (const [key, entry] of cache) {
|
|
608
|
+
if (newKeySet.has(key)) continue;
|
|
609
|
+
if (entry.cleanup) {
|
|
610
|
+
entry.cleanup();
|
|
611
|
+
cleanupCount--;
|
|
612
|
+
}
|
|
613
|
+
entry.anchor.parentNode?.removeChild(entry.anchor);
|
|
614
|
+
cache.delete(key);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const mountNewForEntries = (items, n, newKeys, liveParent) => {
|
|
618
|
+
for (let i = 0; i < n; i++) {
|
|
619
|
+
const key = newKeys[i];
|
|
620
|
+
if (cache.has(key)) continue;
|
|
621
|
+
renderInto(items[i], key, i, liveParent, tailMarker);
|
|
622
|
+
const entry = cache.get(key);
|
|
623
|
+
if (entry) _forAnchors.add(entry.anchor);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const handleFastClear = liveParent => {
|
|
627
|
+
if (cache.size === 0) return;
|
|
628
|
+
if (cleanupCount > 0) {
|
|
629
|
+
for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
|
|
630
|
+
}
|
|
631
|
+
const pp = liveParent.parentNode;
|
|
632
|
+
if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
|
|
633
|
+
const fresh = liveParent.cloneNode(false);
|
|
634
|
+
fresh.appendChild(startMarker);
|
|
635
|
+
fresh.appendChild(tailMarker);
|
|
636
|
+
pp.replaceChild(fresh, liveParent);
|
|
637
|
+
} else clearBetween(startMarker, tailMarker);
|
|
638
|
+
cache = /* @__PURE__ */new Map();
|
|
639
|
+
cleanupCount = 0;
|
|
640
|
+
currentKeys = [];
|
|
641
|
+
};
|
|
642
|
+
const hasAnyKeptKey = (n, newKeys) => {
|
|
643
|
+
for (let i = 0; i < n; i++) if (cache.has(newKeys[i])) return true;
|
|
644
|
+
return false;
|
|
645
|
+
};
|
|
646
|
+
const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
|
|
647
|
+
removeStaleForEntries(new Set(newKeys));
|
|
648
|
+
mountNewForEntries(items, n, newKeys, liveParent);
|
|
649
|
+
if (!anchorsRegistered) {
|
|
650
|
+
for (const entry of cache.values()) _forAnchors.add(entry.anchor);
|
|
651
|
+
anchorsRegistered = true;
|
|
652
|
+
}
|
|
653
|
+
if (trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker)) {
|
|
654
|
+
currentKeys = newKeys;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
lis = forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker);
|
|
658
|
+
currentKeys = newKeys;
|
|
659
|
+
};
|
|
660
|
+
const e = effect(() => {
|
|
661
|
+
const liveParent = startMarker.parentNode;
|
|
662
|
+
if (!liveParent) return;
|
|
663
|
+
const items = source();
|
|
664
|
+
const n = items.length;
|
|
665
|
+
if (n === 0) {
|
|
666
|
+
handleFastClear(liveParent);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (currentKeys.length === 0) {
|
|
670
|
+
handleFreshRender(items, n, liveParent);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const newKeys = collectNewKeys(items, n);
|
|
674
|
+
if (!hasAnyKeptKey(n, newKeys)) {
|
|
675
|
+
handleReplaceAll(items, n, newKeys, liveParent);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
handleIncrementalUpdate(items, n, newKeys, liveParent);
|
|
679
|
+
});
|
|
680
|
+
return () => {
|
|
681
|
+
e.dispose();
|
|
682
|
+
for (const entry of cache.values()) {
|
|
683
|
+
if (cleanupCount > 0 && entry.cleanup) entry.cleanup();
|
|
684
|
+
entry.anchor.parentNode?.removeChild(entry.anchor);
|
|
685
|
+
}
|
|
686
|
+
cache = /* @__PURE__ */new Map();
|
|
687
|
+
cleanupCount = 0;
|
|
688
|
+
startMarker.parentNode?.removeChild(startMarker);
|
|
689
|
+
tailMarker.parentNode?.removeChild(tailMarker);
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Small-k reorder: directly place the k displaced entries without LIS.
|
|
694
|
+
*/
|
|
695
|
+
function smallKPlace(parent, diffs, newKeys, cache, tailMarker) {
|
|
696
|
+
const diffSet = new Set(diffs);
|
|
697
|
+
let cursor = tailMarker;
|
|
698
|
+
let prevDiffIdx = newKeys.length;
|
|
699
|
+
for (let d = diffs.length - 1; d >= 0; d--) {
|
|
700
|
+
const i = diffs[d];
|
|
701
|
+
let nextNonDiff = -1;
|
|
702
|
+
for (let j = i + 1; j < prevDiffIdx; j++) if (!diffSet.has(j)) {
|
|
703
|
+
nextNonDiff = j;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (nextNonDiff >= 0) {
|
|
707
|
+
const nc = cache.get(newKeys[nextNonDiff])?.anchor;
|
|
708
|
+
if (nc) cursor = nc;
|
|
709
|
+
}
|
|
710
|
+
const entry = cache.get(newKeys[i]);
|
|
711
|
+
if (!entry) {
|
|
712
|
+
prevDiffIdx = i;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
moveEntryBefore(parent, entry.anchor, cursor);
|
|
716
|
+
cursor = entry.anchor;
|
|
717
|
+
prevDiffIdx = i;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Move startNode and all siblings belonging to this entry to just before `before`.
|
|
722
|
+
* Stops at the next entry anchor (identified via WeakSet) or the tail marker.
|
|
723
|
+
*
|
|
724
|
+
* Fast path: if the next sibling is already a boundary (another entry or tail),
|
|
725
|
+
* this entry is a single node — skip the toMove array entirely.
|
|
726
|
+
*/
|
|
727
|
+
function moveEntryBefore(parent, startNode, before) {
|
|
728
|
+
const next = startNode.nextSibling;
|
|
729
|
+
if (!next || next === before || next.parentNode === parent && (_forAnchors.has(next) || _keyedAnchors.has(next))) {
|
|
730
|
+
parent.insertBefore(startNode, before);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const toMove = [startNode];
|
|
734
|
+
let cur = next;
|
|
735
|
+
while (cur && cur !== before) {
|
|
736
|
+
const nextNode = cur.nextSibling;
|
|
737
|
+
toMove.push(cur);
|
|
738
|
+
cur = nextNode;
|
|
739
|
+
if (cur && cur.parentNode === parent && (cur === before || _forAnchors.has(cur) || _keyedAnchors.has(cur))) break;
|
|
740
|
+
}
|
|
741
|
+
for (const node of toMove) parent.insertBefore(node, before);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/props.ts
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
|
|
749
|
+
* Overrides both the Sanitizer API and the built-in fallback.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* // With DOMPurify:
|
|
753
|
+
* import DOMPurify from "dompurify"
|
|
754
|
+
* setSanitizer((html) => DOMPurify.sanitize(html))
|
|
755
|
+
*
|
|
756
|
+
* // With sanitize-html:
|
|
757
|
+
* import sanitize from "sanitize-html"
|
|
758
|
+
* setSanitizer((html) => sanitize(html))
|
|
759
|
+
*
|
|
760
|
+
* // Reset to built-in:
|
|
761
|
+
* setSanitizer(null)
|
|
762
|
+
*/
|
|
763
|
+
function setSanitizer(fn) {
|
|
764
|
+
_customSanitizer = fn;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Fallback tag-stripping sanitizer for environments without the Sanitizer API.
|
|
768
|
+
* Removes all tags not in SAFE_TAGS, strips event handler attributes,
|
|
769
|
+
* and blocks javascript:/data: URLs in href/src/action attributes.
|
|
770
|
+
*/
|
|
771
|
+
function fallbackSanitize(html) {
|
|
772
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
773
|
+
sanitizeNode(doc.body);
|
|
774
|
+
return doc.body.innerHTML;
|
|
775
|
+
}
|
|
776
|
+
/** Strip unsafe attributes from a single element. */
|
|
777
|
+
function stripUnsafeAttrs(el) {
|
|
778
|
+
const attrs = Array.from(el.attributes);
|
|
779
|
+
for (const attr of attrs) if (UNSAFE_ATTR_RE.test(attr.name)) el.removeAttribute(attr.name);else if (URL_ATTRS.has(attr.name) && UNSAFE_URL_RE.test(attr.value)) el.removeAttribute(attr.name);
|
|
780
|
+
}
|
|
781
|
+
function sanitizeNode(node) {
|
|
782
|
+
const children = Array.from(node.childNodes);
|
|
783
|
+
for (const child of children) {
|
|
784
|
+
if (child.nodeType !== 1) continue;
|
|
785
|
+
const el = child;
|
|
786
|
+
const tag = el.tagName.toLowerCase();
|
|
787
|
+
if (!SAFE_TAGS.has(tag)) {
|
|
788
|
+
const text = document.createTextNode(el.textContent);
|
|
789
|
+
node.replaceChild(text, el);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
stripUnsafeAttrs(el);
|
|
793
|
+
sanitizeNode(el);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
|
|
798
|
+
* Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
|
|
799
|
+
*/
|
|
800
|
+
function sanitizeHtml(html) {
|
|
801
|
+
if (_customSanitizer) return _customSanitizer(html);
|
|
802
|
+
return fallbackSanitize(html);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Apply all props to a DOM element.
|
|
806
|
+
* Returns a single chained cleanup (or null if no props need teardown).
|
|
807
|
+
* Uses for-in instead of Object.keys() to avoid allocating a keys array.
|
|
808
|
+
*/
|
|
809
|
+
function applyProps(el, props) {
|
|
810
|
+
let cleanup = null;
|
|
811
|
+
for (const key in props) {
|
|
812
|
+
if (key === "key" || key === "ref") continue;
|
|
813
|
+
const c = applyProp(el, key, props[key]);
|
|
814
|
+
if (c) if (!cleanup) cleanup = c;else {
|
|
815
|
+
const prev = cleanup;
|
|
816
|
+
cleanup = () => {
|
|
817
|
+
prev();
|
|
818
|
+
c();
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return cleanup;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Apply a single prop.
|
|
826
|
+
*
|
|
827
|
+
* - `onXxx` → addEventListener
|
|
828
|
+
* - `() => value` (non-event function) → reactive via effect
|
|
829
|
+
* - anything else → static attribute / DOM property
|
|
830
|
+
*/
|
|
831
|
+
function applyProp(el, key, value) {
|
|
832
|
+
if (EVENT_RE.test(key)) {
|
|
833
|
+
const eventName = key[2]?.toLowerCase() + key.slice(3);
|
|
834
|
+
const handler = value;
|
|
835
|
+
const batched = e => batch(() => handler(e));
|
|
836
|
+
el.addEventListener(eventName, batched);
|
|
837
|
+
return () => el.removeEventListener(eventName, batched);
|
|
838
|
+
}
|
|
839
|
+
if (key === "innerHTML") {
|
|
840
|
+
if (typeof el.setHTML === "function") el.setHTML(value);else el.innerHTML = sanitizeHtml(value);
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
if (key === "dangerouslySetInnerHTML") {
|
|
844
|
+
if (__DEV__$3) console.warn("[Pyreon] dangerouslySetInnerHTML bypasses sanitization. Ensure the HTML is trusted.");
|
|
845
|
+
el.innerHTML = value.__html;
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
if (key === "n-show") return renderEffect(() => {
|
|
849
|
+
const visible = value();
|
|
850
|
+
el.style.display = visible ? "" : "none";
|
|
851
|
+
});
|
|
852
|
+
if (key.startsWith("n-")) {
|
|
853
|
+
const directive = value;
|
|
854
|
+
const cleanups = [];
|
|
855
|
+
directive(el, fn => cleanups.push(fn));
|
|
856
|
+
return cleanups.length > 0 ? () => {
|
|
857
|
+
for (const fn of cleanups) fn();
|
|
858
|
+
} : null;
|
|
859
|
+
}
|
|
860
|
+
if (typeof value === "function") return renderEffect(() => setStaticProp(el, key, value()));
|
|
861
|
+
setStaticProp(el, key, value);
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
/** Apply a style prop (string or object). */
|
|
865
|
+
function applyStyleProp(el, value) {
|
|
866
|
+
if (typeof value === "string") el.style.cssText = value;else if (value != null && typeof value === "object") Object.assign(el.style, value);
|
|
867
|
+
}
|
|
868
|
+
function setStaticProp(el, key, value) {
|
|
869
|
+
if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
|
|
870
|
+
if (__DEV__$3) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (key === "class" || key === "className") {
|
|
874
|
+
el.setAttribute("class", value == null ? "" : String(value));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (key === "style") {
|
|
878
|
+
applyStyleProp(el, value);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (value == null) {
|
|
882
|
+
el.removeAttribute(key);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (typeof value === "boolean") {
|
|
886
|
+
if (value) el.setAttribute(key, "");else el.removeAttribute(key);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (key in el) {
|
|
890
|
+
el[key] = value;
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
el.setAttribute(key, String(value));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/mount.ts
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Mount a single child into `parent`, inserting before `anchor` (null = append).
|
|
901
|
+
* Returns a cleanup that removes the node(s) and disposes all reactive effects.
|
|
902
|
+
*
|
|
903
|
+
* This function is the hot path — all child types are handled inline to avoid
|
|
904
|
+
* function call overhead in tight render loops (1000+ calls per list render).
|
|
905
|
+
*/
|
|
906
|
+
function mountChild(child, parent, anchor = null) {
|
|
907
|
+
if (typeof child === "function") {
|
|
908
|
+
const sample = runUntracked(() => child());
|
|
909
|
+
if (isKeyedArray(sample)) {
|
|
910
|
+
const prevDepth = _elementDepth;
|
|
911
|
+
_elementDepth = 0;
|
|
912
|
+
const cleanup = mountKeyedList(child, parent, anchor, (v, p, a) => mountChild(v, p, a));
|
|
913
|
+
_elementDepth = prevDepth;
|
|
914
|
+
return cleanup;
|
|
915
|
+
}
|
|
916
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
917
|
+
const text = document.createTextNode(sample == null || sample === false ? "" : String(sample));
|
|
918
|
+
parent.insertBefore(text, anchor);
|
|
919
|
+
const dispose = renderEffect(() => {
|
|
920
|
+
const v = child();
|
|
921
|
+
text.data = v == null || v === false ? "" : String(v);
|
|
922
|
+
});
|
|
923
|
+
if (_elementDepth > 0) return dispose;
|
|
924
|
+
return () => {
|
|
925
|
+
dispose();
|
|
926
|
+
const p = text.parentNode;
|
|
927
|
+
if (p && p.isConnected !== false) p.removeChild(text);
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const prevDepth = _elementDepth;
|
|
931
|
+
_elementDepth = 0;
|
|
932
|
+
const cleanup = mountReactive(child, parent, anchor, mountChild);
|
|
933
|
+
_elementDepth = prevDepth;
|
|
934
|
+
return cleanup;
|
|
935
|
+
}
|
|
936
|
+
if (Array.isArray(child)) return mountChildren(child, parent, anchor);
|
|
937
|
+
if (child == null || child === false) return noop$1;
|
|
938
|
+
if (typeof child !== "object") {
|
|
939
|
+
parent.insertBefore(document.createTextNode(String(child)), anchor);
|
|
940
|
+
return noop$1;
|
|
941
|
+
}
|
|
942
|
+
if (child.__isNative) {
|
|
943
|
+
const native = child;
|
|
944
|
+
parent.insertBefore(native.el, anchor);
|
|
945
|
+
if (!native.cleanup) {
|
|
946
|
+
if (_elementDepth > 0) return noop$1;
|
|
947
|
+
return () => {
|
|
948
|
+
const p = native.el.parentNode;
|
|
949
|
+
if (p && p.isConnected !== false) p.removeChild(native.el);
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (_elementDepth > 0) return native.cleanup;
|
|
953
|
+
return () => {
|
|
954
|
+
native.cleanup?.();
|
|
955
|
+
const p = native.el.parentNode;
|
|
956
|
+
if (p && p.isConnected !== false) p.removeChild(native.el);
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const vnode = child;
|
|
960
|
+
if (vnode.type === Fragment) return mountChildren(vnode.children, parent, anchor);
|
|
961
|
+
if (vnode.type === ForSymbol) {
|
|
962
|
+
const {
|
|
963
|
+
each,
|
|
964
|
+
by,
|
|
965
|
+
children
|
|
966
|
+
} = vnode.props;
|
|
967
|
+
const prevDepth = _elementDepth;
|
|
968
|
+
_elementDepth = 0;
|
|
969
|
+
const cleanup = mountFor(each, by, children, parent, anchor, mountChild);
|
|
970
|
+
_elementDepth = prevDepth;
|
|
971
|
+
return cleanup;
|
|
972
|
+
}
|
|
973
|
+
if (vnode.type === PortalSymbol) {
|
|
974
|
+
const {
|
|
975
|
+
target,
|
|
976
|
+
children
|
|
977
|
+
} = vnode.props;
|
|
978
|
+
if (__DEV__$2 && !target) return noop$1;
|
|
979
|
+
return mountChild(children, target, null);
|
|
980
|
+
}
|
|
981
|
+
if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
|
|
982
|
+
return mountElement(vnode, parent, anchor);
|
|
983
|
+
}
|
|
984
|
+
function mountElement(vnode, parent, anchor) {
|
|
985
|
+
const el = document.createElement(vnode.type);
|
|
986
|
+
const props = vnode.props;
|
|
987
|
+
const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
|
|
988
|
+
_elementDepth++;
|
|
989
|
+
const childCleanup = mountChildren(vnode.children, el, null);
|
|
990
|
+
_elementDepth--;
|
|
991
|
+
parent.insertBefore(el, anchor);
|
|
992
|
+
const ref = props.ref;
|
|
993
|
+
if (ref && typeof ref === "object") ref.current = el;
|
|
994
|
+
if (!propCleanup && childCleanup === noop$1 && !ref) {
|
|
995
|
+
if (_elementDepth > 0) return noop$1;
|
|
996
|
+
return () => {
|
|
997
|
+
const p = el.parentNode;
|
|
998
|
+
if (p && p.isConnected !== false) p.removeChild(el);
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (_elementDepth > 0) {
|
|
1002
|
+
if (!ref && !propCleanup) return childCleanup;
|
|
1003
|
+
if (!ref && propCleanup) return () => {
|
|
1004
|
+
propCleanup();
|
|
1005
|
+
childCleanup();
|
|
1006
|
+
};
|
|
1007
|
+
const refToClean = ref;
|
|
1008
|
+
return () => {
|
|
1009
|
+
if (refToClean && typeof refToClean === "object") refToClean.current = null;
|
|
1010
|
+
if (propCleanup) propCleanup();
|
|
1011
|
+
childCleanup();
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
return () => {
|
|
1015
|
+
if (ref && typeof ref === "object") ref.current = null;
|
|
1016
|
+
if (propCleanup) propCleanup();
|
|
1017
|
+
childCleanup();
|
|
1018
|
+
const p = el.parentNode;
|
|
1019
|
+
if (p && p.isConnected !== false) p.removeChild(el);
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function mountComponent(vnode, parent, anchor) {
|
|
1023
|
+
const scope = effectScope();
|
|
1024
|
+
setCurrentScope(scope);
|
|
1025
|
+
let hooks;
|
|
1026
|
+
let output;
|
|
1027
|
+
const componentName = vnode.type.name || "Anonymous";
|
|
1028
|
+
const compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1029
|
+
const parentId = _mountingStack[_mountingStack.length - 1] ?? null;
|
|
1030
|
+
_mountingStack.push(compId);
|
|
1031
|
+
const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
|
|
1032
|
+
...vnode.props,
|
|
1033
|
+
children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
|
|
1034
|
+
} : vnode.props;
|
|
1035
|
+
try {
|
|
1036
|
+
const result = runWithHooks(vnode.type, mergedProps);
|
|
1037
|
+
hooks = result.hooks;
|
|
1038
|
+
output = result.vnode;
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
_mountingStack.pop();
|
|
1041
|
+
setCurrentScope(null);
|
|
1042
|
+
scope.stop();
|
|
1043
|
+
reportError({
|
|
1044
|
+
component: componentName,
|
|
1045
|
+
phase: "setup",
|
|
1046
|
+
error: err,
|
|
1047
|
+
timestamp: Date.now(),
|
|
1048
|
+
props: vnode.props
|
|
1049
|
+
});
|
|
1050
|
+
dispatchToErrorBoundary(err);
|
|
1051
|
+
return noop$1;
|
|
1052
|
+
} finally {
|
|
1053
|
+
setCurrentScope(null);
|
|
1054
|
+
}
|
|
1055
|
+
if (__DEV__$2 && output != null && typeof output === "object" && !("type" in output)) console.warn(`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, or function.`);
|
|
1056
|
+
for (const fn of hooks.update) scope.addUpdateHook(fn);
|
|
1057
|
+
let subtreeCleanup = noop$1;
|
|
1058
|
+
try {
|
|
1059
|
+
subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
_mountingStack.pop();
|
|
1062
|
+
scope.stop();
|
|
1063
|
+
if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) reportError({
|
|
1064
|
+
component: componentName,
|
|
1065
|
+
phase: "render",
|
|
1066
|
+
error: err,
|
|
1067
|
+
timestamp: Date.now(),
|
|
1068
|
+
props: vnode.props
|
|
1069
|
+
});
|
|
1070
|
+
return noop$1;
|
|
1071
|
+
}
|
|
1072
|
+
_mountingStack.pop();
|
|
1073
|
+
registerComponent(compId, componentName, parent instanceof Element ? parent.firstElementChild : null, parentId);
|
|
1074
|
+
const mountCleanups = [];
|
|
1075
|
+
for (const fn of hooks.mount) try {
|
|
1076
|
+
let cleanup;
|
|
1077
|
+
scope.runInScope(() => {
|
|
1078
|
+
cleanup = fn();
|
|
1079
|
+
});
|
|
1080
|
+
if (cleanup) mountCleanups.push(cleanup);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
|
|
1083
|
+
reportError({
|
|
1084
|
+
component: componentName,
|
|
1085
|
+
phase: "mount",
|
|
1086
|
+
error: err,
|
|
1087
|
+
timestamp: Date.now()
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
return () => {
|
|
1091
|
+
unregisterComponent(compId);
|
|
1092
|
+
scope.stop();
|
|
1093
|
+
subtreeCleanup();
|
|
1094
|
+
for (const fn of hooks.unmount) try {
|
|
1095
|
+
fn();
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
|
|
1098
|
+
reportError({
|
|
1099
|
+
component: componentName,
|
|
1100
|
+
phase: "unmount",
|
|
1101
|
+
error: err,
|
|
1102
|
+
timestamp: Date.now()
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
for (const fn of mountCleanups) fn();
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function mountChildren(children, parent, anchor) {
|
|
1109
|
+
if (children.length === 0) return noop$1;
|
|
1110
|
+
if (children.length === 1) {
|
|
1111
|
+
const c = children[0];
|
|
1112
|
+
if (c !== void 0) {
|
|
1113
|
+
if (anchor === null && (typeof c === "string" || typeof c === "number")) {
|
|
1114
|
+
parent.textContent = String(c);
|
|
1115
|
+
return noop$1;
|
|
1116
|
+
}
|
|
1117
|
+
return mountChild(c, parent, anchor);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (children.length === 2) {
|
|
1121
|
+
const c0 = children[0];
|
|
1122
|
+
const c1 = children[1];
|
|
1123
|
+
if (c0 !== void 0 && c1 !== void 0) {
|
|
1124
|
+
const d0 = mountChild(c0, parent, anchor);
|
|
1125
|
+
const d1 = mountChild(c1, parent, anchor);
|
|
1126
|
+
if (d0 === noop$1 && d1 === noop$1) return noop$1;
|
|
1127
|
+
if (d0 === noop$1) return d1;
|
|
1128
|
+
if (d1 === noop$1) return d0;
|
|
1129
|
+
return () => {
|
|
1130
|
+
d0();
|
|
1131
|
+
d1();
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const cleanups = children.map(c => mountChild(c, parent, anchor));
|
|
1136
|
+
return () => {
|
|
1137
|
+
for (const fn of cleanups) fn();
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/** Returns true if value is a non-empty array of VNodes that all carry keys. */
|
|
1141
|
+
function isKeyedArray(value) {
|
|
1142
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
1143
|
+
return value.every(v => v !== null && typeof v === "object" && !Array.isArray(v) && v.key !== null && v.key !== void 0);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/hydrate.ts
|
|
1148
|
+
|
|
1149
|
+
/** Skip comment and whitespace-only text nodes, return first "real" node */
|
|
1150
|
+
function firstReal(initialNode) {
|
|
1151
|
+
let node = initialNode;
|
|
1152
|
+
while (node) {
|
|
1153
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
1154
|
+
node = node.nextSibling;
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
if (node.nodeType === Node.TEXT_NODE && node.data.trim() === "") {
|
|
1158
|
+
node = node.nextSibling;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
return node;
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
/** Advance past a node, skipping whitespace-only text and comments */
|
|
1166
|
+
function nextReal(node) {
|
|
1167
|
+
return firstReal(node.nextSibling);
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Hydrate a single VNodeChild against the DOM subtree starting at `domNode`.
|
|
1171
|
+
* Returns [cleanup, nextDomSibling].
|
|
1172
|
+
*/
|
|
1173
|
+
/** Insert a comment marker before domNode (or append if domNode is null). */
|
|
1174
|
+
function insertMarker(parent, domNode, text) {
|
|
1175
|
+
const marker = document.createComment(text);
|
|
1176
|
+
if (domNode) parent.insertBefore(marker, domNode);else parent.appendChild(marker);
|
|
1177
|
+
return marker;
|
|
1178
|
+
}
|
|
1179
|
+
/** Hydrate a reactive accessor (function child). */
|
|
1180
|
+
function hydrateReactiveChild(child, domNode, parent, anchor, path) {
|
|
1181
|
+
const initial = runUntracked(child);
|
|
1182
|
+
if (initial == null || initial === false) return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode];
|
|
1183
|
+
if (typeof initial === "string" || typeof initial === "number" || typeof initial === "boolean") return hydrateReactiveText(child, domNode, parent, anchor, path);
|
|
1184
|
+
return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode ? nextReal(domNode) : null];
|
|
1185
|
+
}
|
|
1186
|
+
/** Hydrate a reactive text binding against an existing text node. */
|
|
1187
|
+
function hydrateReactiveText(child, domNode, parent, anchor, path) {
|
|
1188
|
+
if (domNode?.nodeType === Node.TEXT_NODE) {
|
|
1189
|
+
const textNode = domNode;
|
|
1190
|
+
const e = effect(() => {
|
|
1191
|
+
const v = child();
|
|
1192
|
+
textNode.data = v == null ? "" : String(v);
|
|
1193
|
+
});
|
|
1194
|
+
return [() => e.dispose(), nextReal(domNode)];
|
|
1195
|
+
}
|
|
1196
|
+
warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > reactive`);
|
|
1197
|
+
return [mountChild(child, parent, anchor), domNode];
|
|
1198
|
+
}
|
|
1199
|
+
/** Hydrate a VNode (fragment, For, Portal, component, element). */
|
|
1200
|
+
function hydrateVNode(vnode, domNode, parent, anchor, path) {
|
|
1201
|
+
if (vnode.type === Fragment) return hydrateChildren(vnode.children, domNode, parent, anchor, path);
|
|
1202
|
+
if (vnode.type === ForSymbol) return [mountChild(vnode, parent, insertMarker(parent, domNode, "pyreon-for")), null];
|
|
1203
|
+
if (vnode.type === PortalSymbol) return [mountChild(vnode, parent, anchor), domNode];
|
|
1204
|
+
if (typeof vnode.type === "function") return hydrateComponent(vnode, domNode, parent, anchor, path);
|
|
1205
|
+
if (typeof vnode.type === "string") return hydrateElement(vnode, domNode, parent, anchor, path);
|
|
1206
|
+
return [noop, domNode];
|
|
1207
|
+
}
|
|
1208
|
+
function hydrateChild(child, domNode, parent, anchor, path = "root") {
|
|
1209
|
+
if (Array.isArray(child)) {
|
|
1210
|
+
const cleanups = [];
|
|
1211
|
+
let cursor = domNode;
|
|
1212
|
+
for (const c of child) {
|
|
1213
|
+
const [cleanup, next] = hydrateChild(c, cursor, parent, anchor, path);
|
|
1214
|
+
cleanups.push(cleanup);
|
|
1215
|
+
cursor = next;
|
|
1216
|
+
}
|
|
1217
|
+
return [() => {
|
|
1218
|
+
for (const c of cleanups) c();
|
|
1219
|
+
}, cursor];
|
|
1220
|
+
}
|
|
1221
|
+
if (child == null || child === false) return [noop, domNode];
|
|
1222
|
+
if (typeof child === "function") return hydrateReactiveChild(child, domNode, parent, anchor, path);
|
|
1223
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
1224
|
+
if (domNode?.nodeType === Node.TEXT_NODE) return [() => domNode.remove(), nextReal(domNode)];
|
|
1225
|
+
warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > text`);
|
|
1226
|
+
return [mountChild(child, parent, anchor), domNode];
|
|
1227
|
+
}
|
|
1228
|
+
return hydrateVNode(child, domNode, parent, anchor, path);
|
|
1229
|
+
}
|
|
1230
|
+
function hydrateElement(vnode, domNode, parent, anchor, path = "root") {
|
|
1231
|
+
const elPath = `${path} > ${vnode.type}`;
|
|
1232
|
+
if (domNode?.nodeType === Node.ELEMENT_NODE && domNode.tagName.toLowerCase() === vnode.type) {
|
|
1233
|
+
const el = domNode;
|
|
1234
|
+
const cleanups = [];
|
|
1235
|
+
const propCleanup = applyProps(el, vnode.props);
|
|
1236
|
+
if (propCleanup) cleanups.push(propCleanup);
|
|
1237
|
+
const firstChild = firstReal(el.firstChild);
|
|
1238
|
+
const [childCleanup] = hydrateChildren(vnode.children, firstChild, el, null, elPath);
|
|
1239
|
+
cleanups.push(childCleanup);
|
|
1240
|
+
const ref = vnode.props.ref;
|
|
1241
|
+
if (ref && typeof ref === "object") ref.current = el;
|
|
1242
|
+
const cleanup = () => {
|
|
1243
|
+
if (ref && typeof ref === "object") ref.current = null;
|
|
1244
|
+
for (const c of cleanups) c();
|
|
1245
|
+
el.remove();
|
|
1246
|
+
};
|
|
1247
|
+
return [cleanup, nextReal(domNode)];
|
|
1248
|
+
}
|
|
1249
|
+
const actual = domNode?.nodeType === Node.ELEMENT_NODE ? domNode.tagName.toLowerCase() : domNode?.nodeType ?? "null";
|
|
1250
|
+
warnHydrationMismatch("tag", vnode.type, actual, elPath);
|
|
1251
|
+
return [mountChild(vnode, parent, anchor), domNode];
|
|
1252
|
+
}
|
|
1253
|
+
function hydrateChildren(children, domNode, parent, anchor, path = "root") {
|
|
1254
|
+
const cleanups = [];
|
|
1255
|
+
let cursor = domNode;
|
|
1256
|
+
for (const child of children) {
|
|
1257
|
+
const [cleanup, next] = hydrateChild(child, cursor, parent, anchor, path);
|
|
1258
|
+
cleanups.push(cleanup);
|
|
1259
|
+
cursor = next;
|
|
1260
|
+
}
|
|
1261
|
+
return [() => {
|
|
1262
|
+
for (const c of cleanups) c();
|
|
1263
|
+
}, cursor];
|
|
1264
|
+
}
|
|
1265
|
+
function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
|
|
1266
|
+
const scope = effectScope();
|
|
1267
|
+
setCurrentScope(scope);
|
|
1268
|
+
let subtreeCleanup = noop;
|
|
1269
|
+
const mountCleanups = [];
|
|
1270
|
+
let nextDom = domNode;
|
|
1271
|
+
const componentName = vnode.type.name || "Anonymous";
|
|
1272
|
+
const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
|
|
1273
|
+
...vnode.props,
|
|
1274
|
+
children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
|
|
1275
|
+
} : vnode.props;
|
|
1276
|
+
let result;
|
|
1277
|
+
try {
|
|
1278
|
+
result = runWithHooks(vnode.type, mergedProps);
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
setCurrentScope(null);
|
|
1281
|
+
scope.stop();
|
|
1282
|
+
console.error(`[Pyreon] Error hydrating component <${componentName}>:`, err);
|
|
1283
|
+
reportError({
|
|
1284
|
+
component: componentName,
|
|
1285
|
+
phase: "setup",
|
|
1286
|
+
error: err,
|
|
1287
|
+
timestamp: Date.now(),
|
|
1288
|
+
props: vnode.props
|
|
1289
|
+
});
|
|
1290
|
+
dispatchToErrorBoundary(err);
|
|
1291
|
+
return [noop, domNode];
|
|
1292
|
+
}
|
|
1293
|
+
setCurrentScope(null);
|
|
1294
|
+
const {
|
|
1295
|
+
vnode: output,
|
|
1296
|
+
hooks
|
|
1297
|
+
} = result;
|
|
1298
|
+
for (const fn of hooks.update) scope.addUpdateHook(fn);
|
|
1299
|
+
if (output != null) {
|
|
1300
|
+
const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path);
|
|
1301
|
+
subtreeCleanup = childCleanup;
|
|
1302
|
+
nextDom = next;
|
|
1303
|
+
}
|
|
1304
|
+
for (const fn of hooks.mount) try {
|
|
1305
|
+
let c;
|
|
1306
|
+
scope.runInScope(() => {
|
|
1307
|
+
c = fn();
|
|
1308
|
+
});
|
|
1309
|
+
if (c) mountCleanups.push(c);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
reportError({
|
|
1312
|
+
component: componentName,
|
|
1313
|
+
phase: "mount",
|
|
1314
|
+
error: err,
|
|
1315
|
+
timestamp: Date.now()
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
const cleanup = () => {
|
|
1319
|
+
scope.stop();
|
|
1320
|
+
subtreeCleanup();
|
|
1321
|
+
for (const fn of hooks.unmount) fn();
|
|
1322
|
+
for (const fn of mountCleanups) fn();
|
|
1323
|
+
};
|
|
1324
|
+
return [cleanup, nextDom];
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Hydrate a server-rendered container with a Pyreon VNode tree.
|
|
1328
|
+
*
|
|
1329
|
+
* Reuses existing DOM elements for static structure, attaches event listeners
|
|
1330
|
+
* and reactive effects without re-rendering. Falls back to fresh mount for
|
|
1331
|
+
* dynamic content (reactive conditionals, For lists).
|
|
1332
|
+
*
|
|
1333
|
+
* @example
|
|
1334
|
+
* // Server:
|
|
1335
|
+
* const html = await renderToString(h(App, null))
|
|
1336
|
+
*
|
|
1337
|
+
* // Client:
|
|
1338
|
+
* const unmount = hydrateRoot(document.getElementById("app")!, h(App, null))
|
|
1339
|
+
*/
|
|
1340
|
+
function hydrateRoot(container, vnode) {
|
|
1341
|
+
const [cleanup] = hydrateChild(vnode, firstReal(container.firstChild), container, null);
|
|
1342
|
+
return cleanup;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region src/keep-alive.ts
|
|
1347
|
+
/**
|
|
1348
|
+
* KeepAlive — mounts its children once and keeps them alive even when hidden.
|
|
1349
|
+
*
|
|
1350
|
+
* Unlike conditional rendering (which destroys and recreates component state),
|
|
1351
|
+
* KeepAlive CSS-hides the children while preserving all reactive state,
|
|
1352
|
+
* scroll position, form values, and in-flight async operations.
|
|
1353
|
+
*
|
|
1354
|
+
* Children are mounted imperatively on first activation and are never unmounted
|
|
1355
|
+
* while the KeepAlive itself is mounted.
|
|
1356
|
+
*
|
|
1357
|
+
* Multi-slot pattern (one KeepAlive per route):
|
|
1358
|
+
* @example
|
|
1359
|
+
* h(Fragment, null, [
|
|
1360
|
+
* h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
|
|
1361
|
+
* h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
|
|
1362
|
+
* ])
|
|
1363
|
+
*
|
|
1364
|
+
* With JSX:
|
|
1365
|
+
* @example
|
|
1366
|
+
* <>
|
|
1367
|
+
* <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
|
|
1368
|
+
* <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
|
|
1369
|
+
* </>
|
|
1370
|
+
*/
|
|
1371
|
+
function KeepAlive(props) {
|
|
1372
|
+
const containerRef = createRef();
|
|
1373
|
+
let childCleanup = null;
|
|
1374
|
+
let childMounted = false;
|
|
1375
|
+
onMount(() => {
|
|
1376
|
+
const container = containerRef.current;
|
|
1377
|
+
const e = effect(() => {
|
|
1378
|
+
const isActive = props.active?.() ?? true;
|
|
1379
|
+
if (!childMounted) {
|
|
1380
|
+
childCleanup = mountChild(props.children ?? null, container, null);
|
|
1381
|
+
childMounted = true;
|
|
1382
|
+
}
|
|
1383
|
+
container.style.display = isActive ? "" : "none";
|
|
1384
|
+
});
|
|
1385
|
+
return () => {
|
|
1386
|
+
e.dispose();
|
|
1387
|
+
childCleanup?.();
|
|
1388
|
+
};
|
|
1389
|
+
});
|
|
1390
|
+
return h("div", {
|
|
1391
|
+
ref: containerRef,
|
|
1392
|
+
style: "display: contents"
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region src/template.ts
|
|
1398
|
+
/**
|
|
1399
|
+
* Creates a row/item factory backed by HTML template cloning.
|
|
1400
|
+
*
|
|
1401
|
+
* - The HTML string is parsed exactly once via <template>.innerHTML.
|
|
1402
|
+
* - Each call to the returned factory clones the root element via
|
|
1403
|
+
* cloneNode(true) — ~5-10x faster than createElement + setAttribute.
|
|
1404
|
+
* - `bind` receives the cloned element and the item; it should wire up
|
|
1405
|
+
* reactive effects and return a cleanup function.
|
|
1406
|
+
* - Returns a NativeItem directly (no VNode wrapper) — saves 2 allocations
|
|
1407
|
+
* per row vs the old VNode + props-object + children-array approach.
|
|
1408
|
+
*
|
|
1409
|
+
* @example
|
|
1410
|
+
* const rowTemplate = createTemplate<Row>(
|
|
1411
|
+
* "<tr><td></td><td></td></tr>",
|
|
1412
|
+
* (el, row) => {
|
|
1413
|
+
* const td1 = el.firstChild as HTMLElement
|
|
1414
|
+
* const td2 = td1.nextSibling as HTMLElement
|
|
1415
|
+
* td1.textContent = String(row.id)
|
|
1416
|
+
* const text = td2.firstChild as Text
|
|
1417
|
+
* text.data = row.label()
|
|
1418
|
+
* const unsub = row.label.subscribe(() => { text.data = row.label() })
|
|
1419
|
+
* return unsub
|
|
1420
|
+
* }
|
|
1421
|
+
* )
|
|
1422
|
+
*/
|
|
1423
|
+
function createTemplate(html, bind) {
|
|
1424
|
+
const tmpl = document.createElement("template");
|
|
1425
|
+
tmpl.innerHTML = html;
|
|
1426
|
+
const proto = tmpl.content.firstElementChild;
|
|
1427
|
+
return item => {
|
|
1428
|
+
const el = proto.cloneNode(true);
|
|
1429
|
+
return {
|
|
1430
|
+
__isNative: true,
|
|
1431
|
+
el,
|
|
1432
|
+
cleanup: bind(el, item)
|
|
1433
|
+
};
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Compiler-emitted template instantiation.
|
|
1438
|
+
*
|
|
1439
|
+
* Parses `html` into a <template> element once (cached), then cloneNode(true)
|
|
1440
|
+
* for each call. The `bind` function wires up dynamic attributes, text content,
|
|
1441
|
+
* and event listeners on the cloned element tree. Returns a NativeItem that
|
|
1442
|
+
* mountChild can insert directly — no VNode allocation.
|
|
1443
|
+
*
|
|
1444
|
+
* This is the runtime half of the compiler's template optimisation. The compiler
|
|
1445
|
+
* detects static JSX element trees and emits `_tpl(html, bindFn)` instead of
|
|
1446
|
+
* nested `h()` calls. Benefits:
|
|
1447
|
+
* - cloneNode(true) is ~5-10x faster than sequential createElement + setAttribute
|
|
1448
|
+
* - Zero VNode / props-object / children-array allocations per instance
|
|
1449
|
+
* - Static attributes are baked into the HTML string (no runtime prop application)
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* // Compiler output for: <div class="box"><span>{text()}</span></div>
|
|
1453
|
+
* _tpl('<div class="box"><span></span></div>', (__root) => {
|
|
1454
|
+
* const __e0 = __root.children[0];
|
|
1455
|
+
* const __d0 = _re(() => { __e0.textContent = text(); });
|
|
1456
|
+
* return () => { __d0(); };
|
|
1457
|
+
* })
|
|
1458
|
+
*/
|
|
1459
|
+
function _tpl(html, bind) {
|
|
1460
|
+
let tpl = _tplCache.get(html);
|
|
1461
|
+
if (!tpl) {
|
|
1462
|
+
tpl = document.createElement("template");
|
|
1463
|
+
tpl.innerHTML = html;
|
|
1464
|
+
_tplCache.set(html, tpl);
|
|
1465
|
+
}
|
|
1466
|
+
const el = tpl.content.firstElementChild?.cloneNode(true);
|
|
1467
|
+
return {
|
|
1468
|
+
__isNative: true,
|
|
1469
|
+
el,
|
|
1470
|
+
cleanup: bind(el)
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region src/transition.ts
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Transition — adds CSS enter/leave animation classes to a single child element,
|
|
1479
|
+
* controlled by the reactive `show` prop.
|
|
1480
|
+
*
|
|
1481
|
+
* Class lifecycle:
|
|
1482
|
+
* Enter: {name}-enter-from → (next frame) → {name}-enter-active + {name}-enter-to → cleanup
|
|
1483
|
+
* Leave: {name}-leave-from → (next frame) → {name}-leave-active + {name}-leave-to → unmount
|
|
1484
|
+
*
|
|
1485
|
+
* The child element stays in the DOM during the leave animation and is removed only
|
|
1486
|
+
* after the CSS transition / animation completes.
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* const visible = signal(false)
|
|
1490
|
+
*
|
|
1491
|
+
* h(Transition, { name: "fade", show: () => visible() },
|
|
1492
|
+
* h("div", { class: "modal" }, "content")
|
|
1493
|
+
* )
|
|
1494
|
+
*
|
|
1495
|
+
* // CSS:
|
|
1496
|
+
* // .fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
1497
|
+
* // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
|
|
1498
|
+
*/
|
|
1499
|
+
function Transition(props) {
|
|
1500
|
+
const n = props.name ?? "pyreon";
|
|
1501
|
+
const cls = {
|
|
1502
|
+
ef: props.enterFrom ?? `${n}-enter-from`,
|
|
1503
|
+
ea: props.enterActive ?? `${n}-enter-active`,
|
|
1504
|
+
et: props.enterTo ?? `${n}-enter-to`,
|
|
1505
|
+
lf: props.leaveFrom ?? `${n}-leave-from`,
|
|
1506
|
+
la: props.leaveActive ?? `${n}-leave-active`,
|
|
1507
|
+
lt: props.leaveTo ?? `${n}-leave-to`
|
|
1508
|
+
};
|
|
1509
|
+
const ref = createRef();
|
|
1510
|
+
const isMounted = signal(runUntracked(props.show));
|
|
1511
|
+
let pendingLeaveCancel = null;
|
|
1512
|
+
let initialized = false;
|
|
1513
|
+
const applyEnter = el => {
|
|
1514
|
+
pendingLeaveCancel?.();
|
|
1515
|
+
pendingLeaveCancel = null;
|
|
1516
|
+
props.onBeforeEnter?.(el);
|
|
1517
|
+
el.classList.remove(cls.lf, cls.la, cls.lt);
|
|
1518
|
+
el.classList.add(cls.ef, cls.ea);
|
|
1519
|
+
requestAnimationFrame(() => {
|
|
1520
|
+
el.classList.remove(cls.ef);
|
|
1521
|
+
el.classList.add(cls.et);
|
|
1522
|
+
const done = () => {
|
|
1523
|
+
el.removeEventListener("transitionend", done);
|
|
1524
|
+
el.removeEventListener("animationend", done);
|
|
1525
|
+
el.classList.remove(cls.ea, cls.et);
|
|
1526
|
+
props.onAfterEnter?.(el);
|
|
1527
|
+
};
|
|
1528
|
+
el.addEventListener("transitionend", done, {
|
|
1529
|
+
once: true
|
|
1530
|
+
});
|
|
1531
|
+
el.addEventListener("animationend", done, {
|
|
1532
|
+
once: true
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
};
|
|
1536
|
+
const applyLeave = el => {
|
|
1537
|
+
props.onBeforeLeave?.(el);
|
|
1538
|
+
el.classList.remove(cls.ef, cls.ea, cls.et);
|
|
1539
|
+
el.classList.add(cls.lf, cls.la);
|
|
1540
|
+
requestAnimationFrame(() => {
|
|
1541
|
+
el.classList.remove(cls.lf);
|
|
1542
|
+
el.classList.add(cls.lt);
|
|
1543
|
+
const done = () => {
|
|
1544
|
+
el.removeEventListener("transitionend", done);
|
|
1545
|
+
el.removeEventListener("animationend", done);
|
|
1546
|
+
el.classList.remove(cls.la, cls.lt);
|
|
1547
|
+
pendingLeaveCancel = null;
|
|
1548
|
+
isMounted.set(false);
|
|
1549
|
+
props.onAfterLeave?.(el);
|
|
1550
|
+
};
|
|
1551
|
+
pendingLeaveCancel = () => {
|
|
1552
|
+
el.removeEventListener("transitionend", done);
|
|
1553
|
+
el.removeEventListener("animationend", done);
|
|
1554
|
+
el.classList.remove(cls.lf, cls.la, cls.lt);
|
|
1555
|
+
};
|
|
1556
|
+
el.addEventListener("transitionend", done, {
|
|
1557
|
+
once: true
|
|
1558
|
+
});
|
|
1559
|
+
el.addEventListener("animationend", done, {
|
|
1560
|
+
once: true
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
};
|
|
1564
|
+
const handleVisibilityChange = visible => {
|
|
1565
|
+
if (visible) {
|
|
1566
|
+
if (!isMounted.peek()) isMounted.set(true);
|
|
1567
|
+
queueMicrotask(() => applyEnter(ref.current));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (!isMounted.peek()) return;
|
|
1571
|
+
const el = ref.current;
|
|
1572
|
+
if (!el) {
|
|
1573
|
+
isMounted.set(false);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
applyLeave(el);
|
|
1577
|
+
};
|
|
1578
|
+
effect(() => {
|
|
1579
|
+
const visible = props.show();
|
|
1580
|
+
if (!initialized) {
|
|
1581
|
+
initialized = true;
|
|
1582
|
+
if (visible && props.appear) queueMicrotask(() => applyEnter(ref.current));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
handleVisibilityChange(visible);
|
|
1586
|
+
});
|
|
1587
|
+
onUnmount(() => {
|
|
1588
|
+
pendingLeaveCancel?.();
|
|
1589
|
+
pendingLeaveCancel = null;
|
|
1590
|
+
});
|
|
1591
|
+
const rawChild = props.children;
|
|
1592
|
+
const emptyFragment = h(Fragment, null);
|
|
1593
|
+
return () => {
|
|
1594
|
+
if (!isMounted()) return emptyFragment;
|
|
1595
|
+
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) return rawChild ?? null;
|
|
1596
|
+
const vnode = rawChild;
|
|
1597
|
+
if (typeof vnode.type !== "string") {
|
|
1598
|
+
if (__DEV__$1) console.warn("[Pyreon] Transition child is a component. Wrap it in a DOM element for enter/leave animations to work.");
|
|
1599
|
+
return vnode;
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
...vnode,
|
|
1603
|
+
props: {
|
|
1604
|
+
...vnode.props,
|
|
1605
|
+
ref
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
//#endregion
|
|
1612
|
+
//#region src/transition-group.ts
|
|
1613
|
+
/**
|
|
1614
|
+
* TransitionGroup — animates a keyed reactive list with CSS enter/leave and
|
|
1615
|
+
* FLIP move animations.
|
|
1616
|
+
*
|
|
1617
|
+
* Class lifecycle:
|
|
1618
|
+
* Enter: {name}-enter-from → {name}-enter-active + {name}-enter-to → cleanup
|
|
1619
|
+
* Leave: {name}-leave-from → {name}-leave-active + {name}-leave-to → item removed
|
|
1620
|
+
* Move: {name}-move (applied when an item shifts position)
|
|
1621
|
+
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* const items = signal([{ id: 1 }, { id: 2 }])
|
|
1624
|
+
*
|
|
1625
|
+
* h(TransitionGroup, {
|
|
1626
|
+
* tag: "ul",
|
|
1627
|
+
* name: "list",
|
|
1628
|
+
* items,
|
|
1629
|
+
* keyFn: (item) => item.id,
|
|
1630
|
+
* render: (item) => h("li", { class: "item" }, item.id),
|
|
1631
|
+
* })
|
|
1632
|
+
*
|
|
1633
|
+
* // CSS:
|
|
1634
|
+
* // .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-10px); }
|
|
1635
|
+
* // .list-enter-active, .list-leave-active { transition: all 300ms ease; }
|
|
1636
|
+
* // .list-move { transition: transform 300ms ease; }
|
|
1637
|
+
*/
|
|
1638
|
+
function TransitionGroup(props) {
|
|
1639
|
+
const tag = props.tag ?? "div";
|
|
1640
|
+
const n = props.name ?? "pyreon";
|
|
1641
|
+
const cls = {
|
|
1642
|
+
ef: props.enterFrom ?? `${n}-enter-from`,
|
|
1643
|
+
ea: props.enterActive ?? `${n}-enter-active`,
|
|
1644
|
+
et: props.enterTo ?? `${n}-enter-to`,
|
|
1645
|
+
lf: props.leaveFrom ?? `${n}-leave-from`,
|
|
1646
|
+
la: props.leaveActive ?? `${n}-leave-active`,
|
|
1647
|
+
lt: props.leaveTo ?? `${n}-leave-to`,
|
|
1648
|
+
mv: props.moveClass ?? `${n}-move`
|
|
1649
|
+
};
|
|
1650
|
+
const containerRef = createRef();
|
|
1651
|
+
const entries = /* @__PURE__ */new Map();
|
|
1652
|
+
const ready = signal(false);
|
|
1653
|
+
let firstRun = true;
|
|
1654
|
+
const applyEnter = el => {
|
|
1655
|
+
props.onBeforeEnter?.(el);
|
|
1656
|
+
el.classList.remove(cls.lf, cls.la, cls.lt);
|
|
1657
|
+
el.classList.add(cls.ef, cls.ea);
|
|
1658
|
+
requestAnimationFrame(() => {
|
|
1659
|
+
el.classList.remove(cls.ef);
|
|
1660
|
+
el.classList.add(cls.et);
|
|
1661
|
+
const done = () => {
|
|
1662
|
+
el.removeEventListener("transitionend", done);
|
|
1663
|
+
el.removeEventListener("animationend", done);
|
|
1664
|
+
el.classList.remove(cls.ea, cls.et);
|
|
1665
|
+
props.onAfterEnter?.(el);
|
|
1666
|
+
};
|
|
1667
|
+
el.addEventListener("transitionend", done, {
|
|
1668
|
+
once: true
|
|
1669
|
+
});
|
|
1670
|
+
el.addEventListener("animationend", done, {
|
|
1671
|
+
once: true
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
};
|
|
1675
|
+
const applyLeave = (el, onDone) => {
|
|
1676
|
+
props.onBeforeLeave?.(el);
|
|
1677
|
+
el.classList.remove(cls.ef, cls.ea, cls.et);
|
|
1678
|
+
el.classList.add(cls.lf, cls.la);
|
|
1679
|
+
requestAnimationFrame(() => {
|
|
1680
|
+
el.classList.remove(cls.lf);
|
|
1681
|
+
el.classList.add(cls.lt);
|
|
1682
|
+
const done = () => {
|
|
1683
|
+
el.removeEventListener("transitionend", done);
|
|
1684
|
+
el.removeEventListener("animationend", done);
|
|
1685
|
+
el.classList.remove(cls.la, cls.lt);
|
|
1686
|
+
props.onAfterLeave?.(el);
|
|
1687
|
+
onDone();
|
|
1688
|
+
};
|
|
1689
|
+
el.addEventListener("transitionend", done, {
|
|
1690
|
+
once: true
|
|
1691
|
+
});
|
|
1692
|
+
el.addEventListener("animationend", done, {
|
|
1693
|
+
once: true
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
};
|
|
1697
|
+
/** Start leave animation for removed items. */
|
|
1698
|
+
const processLeaves = newKeys => {
|
|
1699
|
+
for (const [key, entry] of entries) {
|
|
1700
|
+
if (newKeys.has(key) || entry.leaving) continue;
|
|
1701
|
+
entry.leaving = true;
|
|
1702
|
+
const el = entry.ref.current;
|
|
1703
|
+
if (el) applyLeave(el, () => {
|
|
1704
|
+
entry.cleanup();
|
|
1705
|
+
entries.delete(key);
|
|
1706
|
+
});else {
|
|
1707
|
+
entry.cleanup();
|
|
1708
|
+
entries.delete(key);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
/** Mount new items and return the list of newly created entries. */
|
|
1713
|
+
const mountNewItems = (items, container) => {
|
|
1714
|
+
const newEntries = [];
|
|
1715
|
+
for (let i = 0; i < items.length; i++) {
|
|
1716
|
+
const item = items[i];
|
|
1717
|
+
const key = props.keyFn(item, i);
|
|
1718
|
+
if (entries.has(key)) continue;
|
|
1719
|
+
const itemRef = createRef();
|
|
1720
|
+
const rawVNode = runUntracked(() => props.render(item, i));
|
|
1721
|
+
const entry = {
|
|
1722
|
+
key,
|
|
1723
|
+
ref: itemRef,
|
|
1724
|
+
cleanup: mountChild(typeof rawVNode.type === "string" ? {
|
|
1725
|
+
...rawVNode,
|
|
1726
|
+
props: {
|
|
1727
|
+
...rawVNode.props,
|
|
1728
|
+
ref: itemRef
|
|
1729
|
+
}
|
|
1730
|
+
} : rawVNode, container, null),
|
|
1731
|
+
leaving: false
|
|
1732
|
+
};
|
|
1733
|
+
entries.set(key, entry);
|
|
1734
|
+
newEntries.push(entry);
|
|
1735
|
+
}
|
|
1736
|
+
return newEntries;
|
|
1737
|
+
};
|
|
1738
|
+
const startMoveAnimation = el => {
|
|
1739
|
+
requestAnimationFrame(() => {
|
|
1740
|
+
el.classList.add(cls.mv);
|
|
1741
|
+
el.style.transform = "";
|
|
1742
|
+
el.style.transition = "";
|
|
1743
|
+
const done = () => {
|
|
1744
|
+
el.removeEventListener("transitionend", done);
|
|
1745
|
+
el.removeEventListener("animationend", done);
|
|
1746
|
+
el.classList.remove(cls.mv);
|
|
1747
|
+
};
|
|
1748
|
+
el.addEventListener("transitionend", done, {
|
|
1749
|
+
once: true
|
|
1750
|
+
});
|
|
1751
|
+
el.addEventListener("animationend", done, {
|
|
1752
|
+
once: true
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
};
|
|
1756
|
+
const flipEntry = (entry, oldPos) => {
|
|
1757
|
+
if (!entry.ref.current) return;
|
|
1758
|
+
const newPos = entry.ref.current.getBoundingClientRect();
|
|
1759
|
+
const dx = oldPos.left - newPos.left;
|
|
1760
|
+
const dy = oldPos.top - newPos.top;
|
|
1761
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
|
|
1762
|
+
const el = entry.ref.current;
|
|
1763
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
1764
|
+
el.style.transition = "none";
|
|
1765
|
+
startMoveAnimation(el);
|
|
1766
|
+
};
|
|
1767
|
+
/** Apply FLIP move animations for items that shifted position. */
|
|
1768
|
+
const applyFlipMoves = oldPositions => {
|
|
1769
|
+
requestAnimationFrame(() => {
|
|
1770
|
+
for (const [key, entry] of entries) {
|
|
1771
|
+
if (entry.leaving) continue;
|
|
1772
|
+
const oldPos = oldPositions.get(key);
|
|
1773
|
+
if (!oldPos) continue;
|
|
1774
|
+
flipEntry(entry, oldPos);
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
const recordOldPositions = () => {
|
|
1779
|
+
const oldPositions = /* @__PURE__ */new Map();
|
|
1780
|
+
for (const [key, entry] of entries) if (!entry.leaving && entry.ref.current) oldPositions.set(key, entry.ref.current.getBoundingClientRect());
|
|
1781
|
+
return oldPositions;
|
|
1782
|
+
};
|
|
1783
|
+
const reorderEntries = (items, container) => {
|
|
1784
|
+
for (let i = 0; i < items.length; i++) {
|
|
1785
|
+
const key = props.keyFn(items[i], i);
|
|
1786
|
+
const entry = entries.get(key);
|
|
1787
|
+
if (!entry || entry.leaving || !entry.ref.current) continue;
|
|
1788
|
+
container.appendChild(entry.ref.current);
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
const animateNewEntries = newEntries => {
|
|
1792
|
+
for (const entry of newEntries) queueMicrotask(() => {
|
|
1793
|
+
if (entry.ref.current) applyEnter(entry.ref.current);
|
|
1794
|
+
});
|
|
1795
|
+
};
|
|
1796
|
+
const e = effect(() => {
|
|
1797
|
+
if (!ready()) return;
|
|
1798
|
+
const container = containerRef.current;
|
|
1799
|
+
if (!container) return;
|
|
1800
|
+
const items = props.items();
|
|
1801
|
+
const newKeys = new Set(items.map((item, i) => props.keyFn(item, i)));
|
|
1802
|
+
const isFirst = firstRun;
|
|
1803
|
+
firstRun = false;
|
|
1804
|
+
const oldPositions = recordOldPositions();
|
|
1805
|
+
processLeaves(newKeys);
|
|
1806
|
+
const newEntries = mountNewItems(items, container);
|
|
1807
|
+
reorderEntries(items, container);
|
|
1808
|
+
if (!isFirst || props.appear) animateNewEntries(newEntries);
|
|
1809
|
+
if (!isFirst && oldPositions.size > 0) applyFlipMoves(oldPositions);
|
|
1810
|
+
});
|
|
1811
|
+
onMount(() => {
|
|
1812
|
+
ready.set(true);
|
|
1813
|
+
});
|
|
1814
|
+
onUnmount(() => {
|
|
1815
|
+
e.dispose();
|
|
1816
|
+
for (const entry of entries.values()) entry.cleanup();
|
|
1817
|
+
entries.clear();
|
|
1818
|
+
});
|
|
1819
|
+
return h(tag, {
|
|
1820
|
+
ref: containerRef
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
//#endregion
|
|
1825
|
+
//#region src/index.ts
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Mount a VNode tree into a container element.
|
|
1829
|
+
* Clears the container first, then mounts the given child.
|
|
1830
|
+
* Returns an `unmount` function that removes everything and disposes effects.
|
|
1831
|
+
*
|
|
1832
|
+
* @example
|
|
1833
|
+
* const unmount = mount(h("div", null, "Hello Pyreon"), document.getElementById("app")!)
|
|
1834
|
+
*/
|
|
1835
|
+
function mount(root, container) {
|
|
1836
|
+
if (__DEV__ && container == null) throw new Error("[pyreon] mount() called with a null/undefined container. Make sure the element exists in the DOM, e.g. document.getElementById(\"app\")");
|
|
1837
|
+
installDevTools();
|
|
1838
|
+
container.innerHTML = "";
|
|
1839
|
+
return mountChild(root, container, null);
|
|
1840
|
+
}
|
|
1841
|
+
/** Alias for `mount` */
|
|
1842
|
+
|
|
1843
|
+
//#endregion
|
|
1844
|
+
export { KeepAlive, Transition, TransitionGroup, _tpl, applyProp, applyProps, createTemplate, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, render, sanitizeHtml, setSanitizer };
|
|
1845
|
+
//# sourceMappingURL=index.d.ts.map
|