what-core 0.1.1 → 0.2.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/dist/a11y.js +425 -0
- package/dist/animation.js +531 -0
- package/dist/components.js +272 -115
- package/dist/data.js +434 -0
- package/dist/dom.js +635 -424
- package/dist/form.js +441 -0
- package/dist/h.js +191 -138
- package/dist/head.js +59 -42
- package/dist/helpers.js +125 -83
- package/dist/hooks.js +224 -134
- package/dist/index.js +2 -2
- package/dist/reactive.js +150 -107
- package/dist/scheduler.js +241 -0
- package/dist/skeleton.js +363 -0
- package/dist/store.js +113 -55
- package/dist/testing.js +367 -0
- package/dist/what.js +2 -2
- package/index.d.ts +15 -0
- package/package.json +1 -1
- package/src/components.js +93 -0
- package/src/dom.js +47 -15
- package/src/index.js +2 -2
- package/src/store.js +23 -5
package/dist/dom.js
CHANGED
|
@@ -1,443 +1,654 @@
|
|
|
1
|
+
// What Framework - DOM Reconciler
|
|
2
|
+
// Surgical DOM updates. Diff props, diff children, patch only what changed.
|
|
3
|
+
// No virtual DOM tree kept in memory — we diff against the live DOM.
|
|
4
|
+
|
|
1
5
|
import { effect, batch, untrack } from './reactive.js';
|
|
2
6
|
import { errorBoundaryStack, reportError } from './components.js';
|
|
7
|
+
|
|
8
|
+
// SVG elements that need namespace
|
|
9
|
+
const SVG_ELEMENTS = new Set([
|
|
10
|
+
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
|
|
11
|
+
'g', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern', 'image',
|
|
12
|
+
'text', 'tspan', 'textPath', 'foreignObject', 'linearGradient', 'radialGradient', 'stop',
|
|
13
|
+
'marker', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter',
|
|
14
|
+
'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
|
|
15
|
+
'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage',
|
|
16
|
+
'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting',
|
|
17
|
+
'feTile', 'feTurbulence',
|
|
18
|
+
]);
|
|
19
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
20
|
+
|
|
21
|
+
// Mount a component tree into a DOM container
|
|
3
22
|
export function mount(vnode, container) {
|
|
4
|
-
if (typeof container === 'string') {
|
|
5
|
-
container = document.querySelector(container);
|
|
6
|
-
}
|
|
7
|
-
container.textContent = '';
|
|
8
|
-
const node = createDOM(vnode, container);
|
|
9
|
-
if (node) container.appendChild(node);
|
|
10
|
-
return () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
if (typeof container === 'string') {
|
|
24
|
+
container = document.querySelector(container);
|
|
25
|
+
}
|
|
26
|
+
container.textContent = '';
|
|
27
|
+
const node = createDOM(vnode, container);
|
|
28
|
+
if (node) container.appendChild(node);
|
|
29
|
+
return () => {
|
|
30
|
+
// Unmount: clean up effects, remove DOM
|
|
31
|
+
container.textContent = '';
|
|
32
|
+
// Disposal is handled by effect cleanup
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Create DOM from VNode ---
|
|
37
|
+
|
|
38
|
+
function createDOM(vnode, parent, isSvg) {
|
|
39
|
+
if (vnode == null || vnode === false || vnode === true) return null;
|
|
40
|
+
|
|
41
|
+
// Text
|
|
42
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
43
|
+
return document.createTextNode(String(vnode));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Array (fragment)
|
|
47
|
+
if (Array.isArray(vnode)) {
|
|
48
|
+
const frag = document.createDocumentFragment();
|
|
49
|
+
for (const child of vnode) {
|
|
50
|
+
const node = createDOM(child, parent, isSvg);
|
|
51
|
+
if (node) frag.appendChild(node);
|
|
52
|
+
}
|
|
53
|
+
return frag;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Component
|
|
57
|
+
if (typeof vnode.tag === 'function') {
|
|
58
|
+
return createComponent(vnode, parent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect SVG context: either we're already in SVG, or this tag is an SVG element
|
|
62
|
+
const svgContext = isSvg || vnode.tag === 'svg' || SVG_ELEMENTS.has(vnode.tag);
|
|
63
|
+
|
|
64
|
+
// HTML or SVG Element
|
|
65
|
+
const el = svgContext
|
|
66
|
+
? document.createElementNS(SVG_NS, vnode.tag)
|
|
67
|
+
: document.createElement(vnode.tag);
|
|
68
|
+
|
|
69
|
+
applyProps(el, vnode.props, {}, svgContext);
|
|
70
|
+
for (const child of vnode.children) {
|
|
71
|
+
const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
|
|
72
|
+
if (node) el.appendChild(node);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Store vnode on element for diffing
|
|
76
|
+
el._vnode = vnode;
|
|
77
|
+
return el;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Component Rendering ---
|
|
81
|
+
|
|
39
82
|
const componentStack = [];
|
|
83
|
+
|
|
40
84
|
export function getCurrentComponent() {
|
|
41
|
-
return componentStack[componentStack.length - 1];
|
|
85
|
+
return componentStack[componentStack.length - 1];
|
|
42
86
|
}
|
|
87
|
+
|
|
43
88
|
function createComponent(vnode, parent) {
|
|
44
|
-
const { tag: Component, props, children } = vnode;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
const { tag: Component, props, children } = vnode;
|
|
90
|
+
|
|
91
|
+
// Handle special boundary components
|
|
92
|
+
if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
|
|
93
|
+
return createErrorBoundary(vnode, parent);
|
|
94
|
+
}
|
|
95
|
+
if (Component === '__suspense' || vnode.tag === '__suspense') {
|
|
96
|
+
return createSuspenseBoundary(vnode, parent);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Component context for hooks
|
|
100
|
+
const ctx = {
|
|
101
|
+
hooks: [],
|
|
102
|
+
hookIndex: 0,
|
|
103
|
+
effects: [],
|
|
104
|
+
cleanups: [],
|
|
105
|
+
mounted: false,
|
|
106
|
+
disposed: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Create a marker for this component's position
|
|
110
|
+
const marker = document.createComment(`w:${Component.name || 'anon'}`);
|
|
111
|
+
|
|
112
|
+
// Reactive render: re-renders when signals used inside change
|
|
113
|
+
let currentNodes = [];
|
|
114
|
+
|
|
115
|
+
const dispose = effect(() => {
|
|
116
|
+
if (ctx.disposed) return;
|
|
117
|
+
ctx.hookIndex = 0;
|
|
118
|
+
|
|
119
|
+
componentStack.push(ctx);
|
|
120
|
+
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
result = Component({ ...props, children });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
componentStack.pop();
|
|
126
|
+
// Try to report to nearest error boundary
|
|
127
|
+
if (!reportError(error)) {
|
|
128
|
+
// No boundary, re-throw
|
|
129
|
+
console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
componentStack.pop();
|
|
136
|
+
|
|
137
|
+
const vnodes = Array.isArray(result) ? result : [result];
|
|
138
|
+
|
|
139
|
+
if (!ctx.mounted) {
|
|
140
|
+
// Initial mount
|
|
141
|
+
ctx.mounted = true;
|
|
142
|
+
for (const v of vnodes) {
|
|
143
|
+
const node = createDOM(v, parent);
|
|
144
|
+
if (node) {
|
|
145
|
+
currentNodes.push(node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Update: reconcile
|
|
150
|
+
reconcile(marker.parentNode, currentNodes, vnodes, marker);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
ctx.effects.push(dispose);
|
|
155
|
+
|
|
156
|
+
// Return a fragment with marker + rendered nodes
|
|
157
|
+
const frag = document.createDocumentFragment();
|
|
158
|
+
frag.appendChild(marker);
|
|
159
|
+
for (const node of currentNodes) {
|
|
160
|
+
frag.appendChild(node);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return frag;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Error boundary component handler
|
|
98
167
|
function createErrorBoundary(vnode, parent) {
|
|
99
|
-
const { errorState, handleError, fallback, reset } = vnode.props;
|
|
100
|
-
const children = vnode.children;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
vnodes =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
const { errorState, handleError, fallback, reset } = vnode.props;
|
|
169
|
+
const children = vnode.children;
|
|
170
|
+
|
|
171
|
+
const marker = document.createComment('w:errorBoundary');
|
|
172
|
+
let currentNodes = [];
|
|
173
|
+
|
|
174
|
+
// Register this boundary
|
|
175
|
+
const boundary = { handleError };
|
|
176
|
+
|
|
177
|
+
const dispose = effect(() => {
|
|
178
|
+
const error = errorState();
|
|
179
|
+
|
|
180
|
+
// Push boundary to stack so children can find it
|
|
181
|
+
errorBoundaryStack.push(boundary);
|
|
182
|
+
|
|
183
|
+
let vnodes;
|
|
184
|
+
if (error) {
|
|
185
|
+
// Render fallback
|
|
186
|
+
if (typeof fallback === 'function') {
|
|
187
|
+
vnodes = [fallback({ error, reset })];
|
|
188
|
+
} else {
|
|
189
|
+
vnodes = [fallback];
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
vnodes = children;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
errorBoundaryStack.pop();
|
|
196
|
+
|
|
197
|
+
vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
|
|
198
|
+
|
|
199
|
+
if (currentNodes.length === 0) {
|
|
200
|
+
// Initial mount
|
|
201
|
+
for (const v of vnodes) {
|
|
202
|
+
const node = createDOM(v, parent);
|
|
203
|
+
if (node) currentNodes.push(node);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
reconcile(marker.parentNode, currentNodes, vnodes, marker);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const frag = document.createDocumentFragment();
|
|
211
|
+
frag.appendChild(marker);
|
|
212
|
+
for (const node of currentNodes) {
|
|
213
|
+
frag.appendChild(node);
|
|
214
|
+
}
|
|
215
|
+
return frag;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Suspense boundary component handler
|
|
135
219
|
function createSuspenseBoundary(vnode, parent) {
|
|
136
|
-
const { boundary, fallback, loading } = vnode.props;
|
|
137
|
-
const children = vnode.children;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
220
|
+
const { boundary, fallback, loading } = vnode.props;
|
|
221
|
+
const children = vnode.children;
|
|
222
|
+
|
|
223
|
+
const marker = document.createComment('w:suspense');
|
|
224
|
+
let currentNodes = [];
|
|
225
|
+
|
|
226
|
+
const dispose = effect(() => {
|
|
227
|
+
const isLoading = loading();
|
|
228
|
+
const vnodes = isLoading ? [fallback] : children;
|
|
229
|
+
const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
|
|
230
|
+
|
|
231
|
+
if (currentNodes.length === 0) {
|
|
232
|
+
for (const v of normalized) {
|
|
233
|
+
const node = createDOM(v, parent);
|
|
234
|
+
if (node) currentNodes.push(node);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
reconcile(marker.parentNode, currentNodes, normalized, marker);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const frag = document.createDocumentFragment();
|
|
242
|
+
frag.appendChild(marker);
|
|
243
|
+
for (const node of currentNodes) {
|
|
244
|
+
frag.appendChild(node);
|
|
245
|
+
}
|
|
246
|
+
return frag;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Reconciliation ---
|
|
250
|
+
// Diff old DOM nodes against new VNodes, patch in place.
|
|
251
|
+
// Uses keyed reconciliation with LIS (Longest Increasing Subsequence) for minimal DOM moves.
|
|
252
|
+
|
|
160
253
|
function reconcile(parent, oldNodes, newVNodes, beforeMarker) {
|
|
161
|
-
if (!parent) return;
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
254
|
+
if (!parent) return;
|
|
255
|
+
|
|
256
|
+
// Check if we have keyed children
|
|
257
|
+
const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
|
|
258
|
+
|
|
259
|
+
if (hasKeys) {
|
|
260
|
+
reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
|
|
261
|
+
} else {
|
|
262
|
+
reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Unkeyed reconciliation (index-based, fast for static lists)
|
|
169
267
|
function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
|
|
170
|
-
const maxLen = Math.max(oldNodes.length, newVNodes.length);
|
|
171
|
-
const newNodes = [];
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
newNodes.push(
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
268
|
+
const maxLen = Math.max(oldNodes.length, newVNodes.length);
|
|
269
|
+
const newNodes = [];
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < maxLen; i++) {
|
|
272
|
+
const oldNode = oldNodes[i];
|
|
273
|
+
const newVNode = newVNodes[i];
|
|
274
|
+
|
|
275
|
+
if (i >= newVNodes.length) {
|
|
276
|
+
// Remove extra old nodes
|
|
277
|
+
if (oldNode && oldNode.parentNode) {
|
|
278
|
+
oldNode.parentNode.removeChild(oldNode);
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (i >= oldNodes.length) {
|
|
284
|
+
// Append new nodes
|
|
285
|
+
const node = createDOM(newVNode, parent);
|
|
286
|
+
if (node) {
|
|
287
|
+
const ref = getInsertionRef(oldNodes, beforeMarker);
|
|
288
|
+
parent.insertBefore(node, ref);
|
|
289
|
+
newNodes.push(node);
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Patch existing node
|
|
295
|
+
const patched = patchNode(parent, oldNode, newVNode);
|
|
296
|
+
newNodes.push(patched);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update the reference array
|
|
300
|
+
oldNodes.length = 0;
|
|
301
|
+
oldNodes.push(...newNodes);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
|
|
196
305
|
function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
let
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let
|
|
235
|
-
for (let
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
// Build old key -> { node, index } map
|
|
307
|
+
const oldKeyMap = new Map();
|
|
308
|
+
for (let i = 0; i < oldNodes.length; i++) {
|
|
309
|
+
const node = oldNodes[i];
|
|
310
|
+
const key = node._vnode?.key;
|
|
311
|
+
if (key != null) {
|
|
312
|
+
oldKeyMap.set(key, { node, index: i });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const newNodes = [];
|
|
317
|
+
const newLen = newVNodes.length;
|
|
318
|
+
|
|
319
|
+
// First pass: match keys and find reusable nodes
|
|
320
|
+
const sources = new Array(newLen).fill(-1); // Maps new index to old index
|
|
321
|
+
const reused = new Set();
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < newLen; i++) {
|
|
324
|
+
const vnode = newVNodes[i];
|
|
325
|
+
const key = vnode?.key;
|
|
326
|
+
if (key != null && oldKeyMap.has(key)) {
|
|
327
|
+
const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
|
|
328
|
+
sources[i] = oldIndex;
|
|
329
|
+
reused.add(oldIndex);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Remove nodes that aren't reused
|
|
334
|
+
for (let i = 0; i < oldNodes.length; i++) {
|
|
335
|
+
if (!reused.has(i) && oldNodes[i]?.parentNode) {
|
|
336
|
+
oldNodes[i].parentNode.removeChild(oldNodes[i]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Find LIS of old indices to determine which nodes don't need to move
|
|
341
|
+
const lis = longestIncreasingSubsequence(sources.filter(s => s !== -1));
|
|
342
|
+
const lisSet = new Set(lis.map((_, i) => {
|
|
343
|
+
let count = 0;
|
|
344
|
+
for (let j = 0; j < sources.length; j++) {
|
|
345
|
+
if (sources[j] !== -1) {
|
|
346
|
+
if (count === lis[i]) return j;
|
|
347
|
+
count++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return -1;
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
// Build new nodes array and move/create as needed
|
|
354
|
+
let lastInserted = beforeMarker?.nextSibling || null;
|
|
355
|
+
|
|
356
|
+
// Process in reverse order for correct insertion
|
|
357
|
+
for (let i = newLen - 1; i >= 0; i--) {
|
|
358
|
+
const vnode = newVNodes[i];
|
|
359
|
+
const key = vnode?.key;
|
|
360
|
+
const oldEntry = key != null ? oldKeyMap.get(key) : null;
|
|
361
|
+
|
|
362
|
+
if (oldEntry && sources[i] !== -1) {
|
|
363
|
+
// Reuse existing node
|
|
364
|
+
const oldNode = oldEntry.node;
|
|
365
|
+
// Patch props/children
|
|
366
|
+
const patched = patchNode(parent, oldNode, vnode);
|
|
367
|
+
newNodes[i] = patched;
|
|
368
|
+
|
|
369
|
+
// Move if not in LIS
|
|
370
|
+
if (!lisSet.has(i) && patched.parentNode) {
|
|
371
|
+
parent.insertBefore(patched, lastInserted);
|
|
372
|
+
}
|
|
373
|
+
lastInserted = patched;
|
|
374
|
+
} else {
|
|
375
|
+
// Create new node
|
|
376
|
+
const node = createDOM(vnode, parent);
|
|
377
|
+
if (node) {
|
|
378
|
+
parent.insertBefore(node, lastInserted);
|
|
379
|
+
lastInserted = node;
|
|
380
|
+
}
|
|
381
|
+
newNodes[i] = node;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Update the reference array
|
|
386
|
+
oldNodes.length = 0;
|
|
387
|
+
oldNodes.push(...newNodes.filter(Boolean));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Longest Increasing Subsequence - O(n log n)
|
|
391
|
+
// Returns indices of elements that form the LIS
|
|
259
392
|
function longestIncreasingSubsequence(arr) {
|
|
260
|
-
if (arr.length === 0) return [];
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
tails.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (arr[
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
393
|
+
if (arr.length === 0) return [];
|
|
394
|
+
|
|
395
|
+
const n = arr.length;
|
|
396
|
+
const dp = new Array(n).fill(1); // Length of LIS ending at i
|
|
397
|
+
const parent = new Array(n).fill(-1); // Parent index for reconstruction
|
|
398
|
+
const tails = [0]; // Indices of smallest tail elements
|
|
399
|
+
|
|
400
|
+
for (let i = 1; i < n; i++) {
|
|
401
|
+
if (arr[i] > arr[tails[tails.length - 1]]) {
|
|
402
|
+
parent[i] = tails[tails.length - 1];
|
|
403
|
+
tails.push(i);
|
|
404
|
+
} else {
|
|
405
|
+
// Binary search for the smallest element >= arr[i]
|
|
406
|
+
let lo = 0, hi = tails.length - 1;
|
|
407
|
+
while (lo < hi) {
|
|
408
|
+
const mid = (lo + hi) >> 1;
|
|
409
|
+
if (arr[tails[mid]] < arr[i]) lo = mid + 1;
|
|
410
|
+
else hi = mid;
|
|
411
|
+
}
|
|
412
|
+
if (arr[i] < arr[tails[lo]]) {
|
|
413
|
+
if (lo > 0) parent[i] = tails[lo - 1];
|
|
414
|
+
tails[lo] = i;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Reconstruct LIS
|
|
420
|
+
const result = [];
|
|
421
|
+
let k = tails[tails.length - 1];
|
|
422
|
+
while (k !== -1) {
|
|
423
|
+
result.push(k);
|
|
424
|
+
k = parent[k];
|
|
425
|
+
}
|
|
426
|
+
return result.reverse();
|
|
427
|
+
}
|
|
428
|
+
|
|
290
429
|
function getInsertionRef(nodes, marker) {
|
|
291
|
-
if (nodes.length > 0) {
|
|
292
|
-
const last = nodes[nodes.length - 1];
|
|
293
|
-
return last.nextSibling;
|
|
294
|
-
}
|
|
295
|
-
return marker ? marker.nextSibling : null;
|
|
430
|
+
if (nodes.length > 0) {
|
|
431
|
+
const last = nodes[nodes.length - 1];
|
|
432
|
+
return last.nextSibling;
|
|
433
|
+
}
|
|
434
|
+
return marker ? marker.nextSibling : null;
|
|
296
435
|
}
|
|
436
|
+
|
|
297
437
|
function patchNode(parent, domNode, vnode) {
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
438
|
+
// Null/removed
|
|
439
|
+
if (vnode == null || vnode === false || vnode === true) {
|
|
440
|
+
if (domNode && domNode.parentNode) domNode.parentNode.removeChild(domNode);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Text
|
|
445
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
446
|
+
const text = String(vnode);
|
|
447
|
+
if (domNode.nodeType === 3) {
|
|
448
|
+
if (domNode.textContent !== text) domNode.textContent = text;
|
|
449
|
+
return domNode;
|
|
450
|
+
}
|
|
451
|
+
const newNode = document.createTextNode(text);
|
|
452
|
+
parent.replaceChild(newNode, domNode);
|
|
453
|
+
return newNode;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Array
|
|
457
|
+
if (Array.isArray(vnode)) {
|
|
458
|
+
// For now, flatten and reconcile as children
|
|
459
|
+
const frag = document.createDocumentFragment();
|
|
460
|
+
for (const v of vnode) {
|
|
461
|
+
const node = createDOM(v, parent);
|
|
462
|
+
if (node) frag.appendChild(node);
|
|
463
|
+
}
|
|
464
|
+
parent.replaceChild(frag, domNode);
|
|
465
|
+
return frag;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Component
|
|
469
|
+
if (typeof vnode.tag === 'function') {
|
|
470
|
+
// Re-create component (future: memoize + diff props)
|
|
471
|
+
const node = createComponent(vnode, parent);
|
|
472
|
+
parent.replaceChild(node, domNode);
|
|
473
|
+
return node;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Element: same tag? Patch props + children
|
|
477
|
+
if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
|
|
478
|
+
const oldProps = domNode._vnode?.props || {};
|
|
479
|
+
applyProps(domNode, vnode.props, oldProps);
|
|
480
|
+
reconcileChildren(domNode, vnode.children);
|
|
481
|
+
domNode._vnode = vnode;
|
|
482
|
+
return domNode;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Different tag: replace entirely
|
|
486
|
+
const newNode = createDOM(vnode, parent);
|
|
487
|
+
parent.replaceChild(newNode, domNode);
|
|
488
|
+
return newNode;
|
|
489
|
+
}
|
|
490
|
+
|
|
337
491
|
function reconcileChildren(parent, newChildVNodes) {
|
|
338
|
-
const oldChildren = Array.from(parent.childNodes);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (i >=
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
el
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
el.
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
492
|
+
const oldChildren = Array.from(parent.childNodes);
|
|
493
|
+
|
|
494
|
+
// Check for keyed children
|
|
495
|
+
const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
|
|
496
|
+
|
|
497
|
+
if (hasKeys) {
|
|
498
|
+
// Use keyed reconciliation
|
|
499
|
+
reconcileKeyed(parent, oldChildren, newChildVNodes, null);
|
|
500
|
+
} else {
|
|
501
|
+
// Unkeyed reconciliation
|
|
502
|
+
const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
|
|
503
|
+
|
|
504
|
+
for (let i = 0; i < maxLen; i++) {
|
|
505
|
+
if (i >= newChildVNodes.length) {
|
|
506
|
+
// Remove extra
|
|
507
|
+
if (oldChildren[i]?.parentNode) {
|
|
508
|
+
parent.removeChild(oldChildren[i]);
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (i >= oldChildren.length) {
|
|
514
|
+
// Append new
|
|
515
|
+
const node = createDOM(newChildVNodes[i], parent);
|
|
516
|
+
if (node) parent.appendChild(node);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
patchNode(parent, oldChildren[i], newChildVNodes[i]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// --- Prop Diffing ---
|
|
526
|
+
// Only touch DOM for props that actually changed.
|
|
527
|
+
|
|
528
|
+
function applyProps(el, newProps, oldProps, isSvg) {
|
|
529
|
+
newProps = newProps || {};
|
|
530
|
+
oldProps = oldProps || {};
|
|
531
|
+
|
|
532
|
+
// Remove old props not in new
|
|
533
|
+
for (const key in oldProps) {
|
|
534
|
+
if (key === 'key' || key === 'ref' || key === 'children') continue;
|
|
535
|
+
if (!(key in newProps)) {
|
|
536
|
+
removeProp(el, key, oldProps[key]);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Set new/changed props
|
|
541
|
+
for (const key in newProps) {
|
|
542
|
+
if (key === 'key' || key === 'ref' || key === 'children') continue;
|
|
543
|
+
if (newProps[key] !== oldProps[key]) {
|
|
544
|
+
setProp(el, key, newProps[key], isSvg);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Handle ref
|
|
549
|
+
if (newProps.ref && newProps.ref !== oldProps.ref) {
|
|
550
|
+
if (typeof newProps.ref === 'function') newProps.ref(el);
|
|
551
|
+
else newProps.ref.current = el;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function setProp(el, key, value, isSvg) {
|
|
556
|
+
// Event handlers: onClick -> click
|
|
557
|
+
// Wrap in untrack so signal reads in handlers don't create subscriptions
|
|
558
|
+
if (key.startsWith('on') && key.length > 2) {
|
|
559
|
+
const event = key.slice(2).toLowerCase();
|
|
560
|
+
// Store handler for removal
|
|
561
|
+
const old = el._events?.[event];
|
|
562
|
+
if (old) el.removeEventListener(event, old);
|
|
563
|
+
if (!el._events) el._events = {};
|
|
564
|
+
// Wrap handler to untrack signal reads
|
|
565
|
+
const wrappedHandler = (e) => untrack(() => value(e));
|
|
566
|
+
wrappedHandler._original = value;
|
|
567
|
+
el._events[event] = wrappedHandler;
|
|
568
|
+
// Check for _eventOpts (once/capture/passive from compiler)
|
|
569
|
+
const eventOpts = value._eventOpts;
|
|
570
|
+
el.addEventListener(event, wrappedHandler, eventOpts || undefined);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// className / class
|
|
575
|
+
if (key === 'className' || key === 'class') {
|
|
576
|
+
if (isSvg) {
|
|
577
|
+
el.setAttribute('class', value || '');
|
|
578
|
+
} else {
|
|
579
|
+
el.className = value || '';
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Style object
|
|
585
|
+
if (key === 'style') {
|
|
586
|
+
if (typeof value === 'string') {
|
|
587
|
+
el.style.cssText = value;
|
|
588
|
+
} else if (typeof value === 'object') {
|
|
589
|
+
for (const prop in value) {
|
|
590
|
+
el.style[prop] = value[prop] ?? '';
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// dangerouslySetInnerHTML
|
|
597
|
+
if (key === 'dangerouslySetInnerHTML') {
|
|
598
|
+
el.innerHTML = value.__html;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Boolean attributes
|
|
603
|
+
if (typeof value === 'boolean') {
|
|
604
|
+
if (value) el.setAttribute(key, '');
|
|
605
|
+
else el.removeAttribute(key);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// data-* and aria-* as attributes
|
|
610
|
+
if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
611
|
+
el.setAttribute(key, value);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// SVG: always use setAttribute (SVG properties don't work as DOM properties)
|
|
616
|
+
if (isSvg) {
|
|
617
|
+
if (value === false || value == null) {
|
|
618
|
+
el.removeAttribute(key);
|
|
619
|
+
} else {
|
|
620
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Default: set as property if it exists, otherwise attribute
|
|
626
|
+
if (key in el) {
|
|
627
|
+
el[key] = value;
|
|
628
|
+
} else {
|
|
629
|
+
el.setAttribute(key, value);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
425
633
|
function removeProp(el, key, oldValue) {
|
|
426
|
-
if (key.startsWith('on') && key.length > 2) {
|
|
427
|
-
const event = key.slice(2).toLowerCase();
|
|
428
|
-
if (el._events?.[event]) {
|
|
429
|
-
el.removeEventListener(event, el._events[event]);
|
|
430
|
-
delete el._events[event];
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
634
|
+
if (key.startsWith('on') && key.length > 2) {
|
|
635
|
+
const event = key.slice(2).toLowerCase();
|
|
636
|
+
if (el._events?.[event]) {
|
|
637
|
+
el.removeEventListener(event, el._events[event]);
|
|
638
|
+
delete el._events[event];
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (key === 'className' || key === 'class') {
|
|
644
|
+
el.className = '';
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (key === 'style') {
|
|
649
|
+
el.style.cssText = '';
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
el.removeAttribute(key);
|
|
441
654
|
}
|
|
442
|
-
el.removeAttribute(key);
|
|
443
|
-
}
|