what-core 0.3.0 → 0.4.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 +22 -7
- package/dist/animation.js +9 -1
- package/dist/components.js +61 -23
- package/dist/data.js +253 -59
- package/dist/dom.js +196 -24
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +63 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +189 -29
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +18 -8
- package/package.json +4 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +9 -1
- package/src/components.js +61 -23
- package/src/data.js +253 -59
- package/src/dom.js +196 -24
- package/src/form.js +112 -44
- package/src/helpers.js +73 -10
- package/src/hooks.js +63 -22
- package/src/index.js +6 -2
- package/src/reactive.js +189 -29
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +18 -8
package/dist/render.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
// What Framework - Fine-Grained Rendering Primitives
|
|
2
|
+
// Solid-style rendering: components run once, signals create individual DOM effects.
|
|
3
|
+
// No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
|
|
4
|
+
|
|
5
|
+
import { effect, untrack, createRoot, signal } from './reactive.js';
|
|
6
|
+
|
|
7
|
+
// --- template(html) ---
|
|
8
|
+
// Pre-parse HTML string into a <template> element. Returns a factory function
|
|
9
|
+
// that clones the DOM tree via cloneNode(true) — 2-5x faster than createElement chains.
|
|
10
|
+
|
|
11
|
+
export function template(html) {
|
|
12
|
+
const t = document.createElement('template');
|
|
13
|
+
t.innerHTML = html.trim();
|
|
14
|
+
return () => t.content.firstChild.cloneNode(true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- insert(parent, child, marker?) ---
|
|
18
|
+
// Reactive child insertion. Handles all child types:
|
|
19
|
+
// - string/number → text node
|
|
20
|
+
// - function → effect that updates text node reactively
|
|
21
|
+
// - DOM node → append directly
|
|
22
|
+
// - array → insert each element
|
|
23
|
+
|
|
24
|
+
export function insert(parent, child, marker) {
|
|
25
|
+
if (child == null || typeof child === 'boolean') return;
|
|
26
|
+
|
|
27
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
28
|
+
const textNode = document.createTextNode(String(child));
|
|
29
|
+
parent.insertBefore(textNode, marker || null);
|
|
30
|
+
return textNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof child === 'function') {
|
|
34
|
+
// Reactive expression — create micro-effect
|
|
35
|
+
let currentNode = document.createTextNode('');
|
|
36
|
+
parent.insertBefore(currentNode, marker || null);
|
|
37
|
+
|
|
38
|
+
effect(() => {
|
|
39
|
+
const value = child();
|
|
40
|
+
if (value instanceof Node) {
|
|
41
|
+
// Function returned a DOM node — replace text node with it
|
|
42
|
+
if (currentNode !== value) {
|
|
43
|
+
parent.replaceChild(value, currentNode);
|
|
44
|
+
currentNode = value;
|
|
45
|
+
}
|
|
46
|
+
} else if (Array.isArray(value)) {
|
|
47
|
+
// Function returned array — handle dynamic lists
|
|
48
|
+
_insertArray(parent, value, currentNode, marker);
|
|
49
|
+
} else {
|
|
50
|
+
// Primitive — update text content
|
|
51
|
+
const text = value == null || typeof value === 'boolean' ? '' : String(value);
|
|
52
|
+
if (currentNode.nodeType === 3) {
|
|
53
|
+
if (currentNode.textContent !== text) currentNode.textContent = text;
|
|
54
|
+
} else {
|
|
55
|
+
const textNode = document.createTextNode(text);
|
|
56
|
+
parent.replaceChild(textNode, currentNode);
|
|
57
|
+
currentNode = textNode;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return currentNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (child instanceof Node) {
|
|
66
|
+
parent.insertBefore(child, marker || null);
|
|
67
|
+
return child;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(child)) {
|
|
71
|
+
const nodes = [];
|
|
72
|
+
for (let i = 0; i < child.length; i++) {
|
|
73
|
+
const node = insert(parent, child[i], marker);
|
|
74
|
+
if (node) nodes.push(node);
|
|
75
|
+
}
|
|
76
|
+
return nodes;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _insertArray(parent, arr, currentNode, marker) {
|
|
81
|
+
// Simple case: replace placeholder with array nodes
|
|
82
|
+
const frag = document.createDocumentFragment();
|
|
83
|
+
for (let i = 0; i < arr.length; i++) {
|
|
84
|
+
if (arr[i] instanceof Node) {
|
|
85
|
+
frag.appendChild(arr[i]);
|
|
86
|
+
} else if (arr[i] != null && typeof arr[i] !== 'boolean') {
|
|
87
|
+
frag.appendChild(document.createTextNode(String(arr[i])));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parent.replaceChild(frag, currentNode);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- mapArray(source, mapFn, options?) ---
|
|
94
|
+
// Reactive list rendering with per-item scopes.
|
|
95
|
+
// Unkeyed: tracks items by reference. Keyed: tracks by key function.
|
|
96
|
+
// With key + raw: mapFn receives (item, index) — raw item value. Items identified by key for
|
|
97
|
+
// efficient DOM reuse/moves. Use when items have per-field signals (no wrapper needed).
|
|
98
|
+
// With key (no raw): mapFn receives (itemAccessor, index) — accessor is a signal getter.
|
|
99
|
+
// When item reference changes but key persists, the signal updates in place.
|
|
100
|
+
// Without key: mapFn receives (item, index) — raw item value. New reference = new row.
|
|
101
|
+
|
|
102
|
+
export function mapArray(source, mapFn, options) {
|
|
103
|
+
const keyFn = options?.key;
|
|
104
|
+
const raw = options?.raw || false;
|
|
105
|
+
|
|
106
|
+
return (parent, marker) => {
|
|
107
|
+
let items = [];
|
|
108
|
+
let mappedNodes = [];
|
|
109
|
+
let disposeFns = [];
|
|
110
|
+
// Keyed mode state: key → { itemSignal }. Null for raw/unkeyed modes.
|
|
111
|
+
let keyedState = keyFn && !raw ? new Map() : null;
|
|
112
|
+
|
|
113
|
+
const endMarker = document.createComment('/list');
|
|
114
|
+
parent.insertBefore(endMarker, marker || null);
|
|
115
|
+
|
|
116
|
+
effect(() => {
|
|
117
|
+
const newItems = source() || [];
|
|
118
|
+
if (keyFn) {
|
|
119
|
+
reconcileKeyed(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState);
|
|
120
|
+
} else {
|
|
121
|
+
reconcileList(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
|
|
122
|
+
}
|
|
123
|
+
items = newItems.slice();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return endMarker;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn) {
|
|
131
|
+
const newLen = newItems.length;
|
|
132
|
+
const oldLen = oldItems.length;
|
|
133
|
+
|
|
134
|
+
if (newLen === 0) {
|
|
135
|
+
// Fast path: clear all
|
|
136
|
+
if (oldLen > 0) {
|
|
137
|
+
for (let i = 0; i < oldLen; i++) disposeFns[i]?.();
|
|
138
|
+
parent.textContent = '';
|
|
139
|
+
parent.appendChild(endMarker);
|
|
140
|
+
mappedNodes.length = 0;
|
|
141
|
+
disposeFns.length = 0;
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (oldLen === 0) {
|
|
147
|
+
// Fast path: all new
|
|
148
|
+
const frag = document.createDocumentFragment();
|
|
149
|
+
for (let i = 0; i < newLen; i++) {
|
|
150
|
+
const item = newItems[i];
|
|
151
|
+
const node = createRoot(dispose => {
|
|
152
|
+
disposeFns[i] = dispose;
|
|
153
|
+
return mapFn(item, i);
|
|
154
|
+
});
|
|
155
|
+
mappedNodes[i] = node;
|
|
156
|
+
frag.appendChild(node);
|
|
157
|
+
}
|
|
158
|
+
parent.insertBefore(frag, endMarker);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Common prefix/suffix skip ---
|
|
163
|
+
let start = 0;
|
|
164
|
+
const minLen = Math.min(oldLen, newLen);
|
|
165
|
+
while (start < minLen && oldItems[start] === newItems[start]) start++;
|
|
166
|
+
|
|
167
|
+
// If everything matches and same length, nothing changed
|
|
168
|
+
if (start === oldLen && start === newLen) return;
|
|
169
|
+
|
|
170
|
+
let oldEnd = oldLen - 1;
|
|
171
|
+
let newEnd = newLen - 1;
|
|
172
|
+
while (oldEnd >= start && newEnd >= start && oldItems[oldEnd] === newItems[newEnd]) {
|
|
173
|
+
oldEnd--;
|
|
174
|
+
newEnd--;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Copy prefix/suffix into output arrays
|
|
178
|
+
const newMapped = new Array(newLen);
|
|
179
|
+
const newDispose = new Array(newLen);
|
|
180
|
+
for (let i = 0; i < start; i++) {
|
|
181
|
+
newMapped[i] = mappedNodes[i];
|
|
182
|
+
newDispose[i] = disposeFns[i];
|
|
183
|
+
}
|
|
184
|
+
for (let i = newEnd + 1; i < newLen; i++) {
|
|
185
|
+
// Suffix items: same item, possibly different index offset
|
|
186
|
+
const oldI = oldEnd + 1 + (i - newEnd - 1);
|
|
187
|
+
newMapped[i] = mappedNodes[oldI];
|
|
188
|
+
newDispose[i] = disposeFns[oldI];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Only reconcile the middle section: start..newEnd (new) vs start..oldEnd (old)
|
|
192
|
+
const midNewLen = newEnd - start + 1;
|
|
193
|
+
const midOldLen = oldEnd - start + 1;
|
|
194
|
+
|
|
195
|
+
if (midNewLen === 0) {
|
|
196
|
+
// Only removals in the middle
|
|
197
|
+
for (let i = start; i <= oldEnd; i++) {
|
|
198
|
+
disposeFns[i]?.();
|
|
199
|
+
if (mappedNodes[i]?.parentNode) mappedNodes[i].parentNode.removeChild(mappedNodes[i]);
|
|
200
|
+
}
|
|
201
|
+
} else if (midOldLen === 0) {
|
|
202
|
+
// Only insertions in the middle
|
|
203
|
+
const marker = start < newLen && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
|
|
204
|
+
const frag = document.createDocumentFragment();
|
|
205
|
+
for (let i = start; i <= newEnd; i++) {
|
|
206
|
+
const item = newItems[i];
|
|
207
|
+
const idx = i;
|
|
208
|
+
newMapped[i] = createRoot(dispose => {
|
|
209
|
+
newDispose[idx] = dispose;
|
|
210
|
+
return mapFn(item, idx);
|
|
211
|
+
});
|
|
212
|
+
frag.appendChild(newMapped[i]);
|
|
213
|
+
}
|
|
214
|
+
parent.insertBefore(frag, marker);
|
|
215
|
+
} else {
|
|
216
|
+
// General case: reconcile middle section with LIS
|
|
217
|
+
_reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns,
|
|
218
|
+
mapFn, start, oldEnd, newEnd, newMapped, newDispose);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update arrays in place
|
|
222
|
+
mappedNodes.length = newLen;
|
|
223
|
+
disposeFns.length = newLen;
|
|
224
|
+
for (let i = 0; i < newLen; i++) {
|
|
225
|
+
mappedNodes[i] = newMapped[i];
|
|
226
|
+
disposeFns[i] = newDispose[i];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns,
|
|
231
|
+
mapFn, start, oldEnd, newEnd, newMapped, newDispose) {
|
|
232
|
+
// Build index map only for the middle section
|
|
233
|
+
const oldIdxMap = new Map();
|
|
234
|
+
for (let i = start; i <= oldEnd; i++) {
|
|
235
|
+
oldIdxMap.set(oldItems[i], i);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Match old items to new positions, collect old indices for LIS
|
|
239
|
+
const midLen = newEnd - start + 1;
|
|
240
|
+
const oldIndices = new Int32Array(midLen); // -1 = new item
|
|
241
|
+
oldIndices.fill(-1);
|
|
242
|
+
|
|
243
|
+
for (let i = start; i <= newEnd; i++) {
|
|
244
|
+
const oldIdx = oldIdxMap.get(newItems[i]);
|
|
245
|
+
if (oldIdx !== undefined) {
|
|
246
|
+
oldIdxMap.delete(newItems[i]);
|
|
247
|
+
newMapped[i] = mappedNodes[oldIdx];
|
|
248
|
+
newDispose[i] = disposeFns[oldIdx];
|
|
249
|
+
oldIndices[i - start] = oldIdx;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Dispose removed items
|
|
254
|
+
for (const [, oldIdx] of oldIdxMap) {
|
|
255
|
+
disposeFns[oldIdx]?.();
|
|
256
|
+
if (mappedNodes[oldIdx]?.parentNode) mappedNodes[oldIdx].parentNode.removeChild(mappedNodes[oldIdx]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Compute LIS on old indices of reused items
|
|
260
|
+
// Build the sequence of old indices for reused items only
|
|
261
|
+
const reusedCount = midLen - _countNeg1(oldIndices, midLen);
|
|
262
|
+
|
|
263
|
+
// Use a bitfield (via Uint8Array) to mark LIS positions — avoids Set overhead
|
|
264
|
+
const inLIS = new Uint8Array(midLen);
|
|
265
|
+
|
|
266
|
+
if (reusedCount > 1) {
|
|
267
|
+
const seq = new Int32Array(reusedCount);
|
|
268
|
+
const seqToMid = new Int32Array(reusedCount); // maps seq index → mid index
|
|
269
|
+
let k = 0;
|
|
270
|
+
for (let i = 0; i < midLen; i++) {
|
|
271
|
+
if (oldIndices[i] !== -1) {
|
|
272
|
+
seq[k] = oldIndices[i];
|
|
273
|
+
seqToMid[k] = i;
|
|
274
|
+
k++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const lisResult = _lis(seq, reusedCount);
|
|
278
|
+
for (let i = 0; i < lisResult.length; i++) {
|
|
279
|
+
inLIS[seqToMid[lisResult[i]]] = 1;
|
|
280
|
+
}
|
|
281
|
+
} else if (reusedCount === 1) {
|
|
282
|
+
// Single reused item is trivially in LIS
|
|
283
|
+
for (let i = 0; i < midLen; i++) {
|
|
284
|
+
if (oldIndices[i] !== -1) { inLIS[i] = 1; break; }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Create new items
|
|
289
|
+
for (let i = start; i <= newEnd; i++) {
|
|
290
|
+
if (!newMapped[i]) {
|
|
291
|
+
const item = newItems[i];
|
|
292
|
+
const idx = i;
|
|
293
|
+
newMapped[i] = createRoot(dispose => {
|
|
294
|
+
newDispose[idx] = dispose;
|
|
295
|
+
return mapFn(item, idx);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Position: work backwards from the item after newEnd (suffix start or endMarker)
|
|
301
|
+
let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1]
|
|
302
|
+
? newMapped[newEnd + 1] : endMarker;
|
|
303
|
+
|
|
304
|
+
for (let i = newEnd; i >= start; i--) {
|
|
305
|
+
const mi = i - start;
|
|
306
|
+
if (oldIndices[mi] === -1 || !inLIS[mi]) {
|
|
307
|
+
// New item or moved item — insert
|
|
308
|
+
parent.insertBefore(newMapped[i], nextSibling);
|
|
309
|
+
}
|
|
310
|
+
nextSibling = newMapped[i];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function _countNeg1(arr, len) {
|
|
315
|
+
let c = 0;
|
|
316
|
+
for (let i = 0; i < len; i++) if (arr[i] === -1) c++;
|
|
317
|
+
return c;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Longest Increasing Subsequence — returns indices into the input array.
|
|
321
|
+
// O(n log n) using patience sorting. Uses typed arrays for performance.
|
|
322
|
+
function _lis(arr, len) {
|
|
323
|
+
if (len === 0) return [];
|
|
324
|
+
if (len === 1) return [0];
|
|
325
|
+
|
|
326
|
+
const tails = new Int32Array(len); // indices into arr
|
|
327
|
+
const predecessors = new Int32Array(len);
|
|
328
|
+
let tailLen = 1;
|
|
329
|
+
tails[0] = 0;
|
|
330
|
+
predecessors[0] = -1;
|
|
331
|
+
|
|
332
|
+
for (let i = 1; i < len; i++) {
|
|
333
|
+
if (arr[i] > arr[tails[tailLen - 1]]) {
|
|
334
|
+
predecessors[i] = tails[tailLen - 1];
|
|
335
|
+
tails[tailLen++] = i;
|
|
336
|
+
} else {
|
|
337
|
+
let lo = 0, hi = tailLen - 1;
|
|
338
|
+
while (lo < hi) {
|
|
339
|
+
const mid = (lo + hi) >> 1;
|
|
340
|
+
if (arr[tails[mid]] < arr[i]) lo = mid + 1;
|
|
341
|
+
else hi = mid;
|
|
342
|
+
}
|
|
343
|
+
tails[lo] = i;
|
|
344
|
+
predecessors[i] = lo > 0 ? tails[lo - 1] : -1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = new Array(tailLen);
|
|
349
|
+
let k = tails[tailLen - 1];
|
|
350
|
+
for (let i = tailLen - 1; i >= 0; i--) {
|
|
351
|
+
result[i] = k;
|
|
352
|
+
k = predecessors[k];
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- reconcileKeyed ---
|
|
358
|
+
// Keyed reconciliation: tracks items by key function, not by reference.
|
|
359
|
+
// When a key persists but its item reference changes, the item signal updates
|
|
360
|
+
// in place — no DOM node destruction/creation. Only effects reading the
|
|
361
|
+
// item accessor re-run (e.g., textContent update for changed label).
|
|
362
|
+
|
|
363
|
+
function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState) {
|
|
364
|
+
const newLen = newItems.length;
|
|
365
|
+
const oldLen = oldItems.length;
|
|
366
|
+
|
|
367
|
+
// --- Fast path: clear all ---
|
|
368
|
+
if (newLen === 0) {
|
|
369
|
+
if (oldLen > 0) {
|
|
370
|
+
// Skip individual disposal: per-row effects only subscribe to their item signal,
|
|
371
|
+
// which is also being discarded. Both become unreachable → GC collects them.
|
|
372
|
+
// Bulk DOM removal: clear parent, re-add marker.
|
|
373
|
+
parent.textContent = '';
|
|
374
|
+
parent.appendChild(endMarker);
|
|
375
|
+
mappedNodes.length = 0;
|
|
376
|
+
disposeFns.length = 0;
|
|
377
|
+
if (keyedState) keyedState.clear();
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- Fast path: all new ---
|
|
383
|
+
if (oldLen === 0) {
|
|
384
|
+
const frag = document.createDocumentFragment();
|
|
385
|
+
for (let i = 0; i < newLen; i++) {
|
|
386
|
+
const item = newItems[i];
|
|
387
|
+
const idx = i;
|
|
388
|
+
let accessor;
|
|
389
|
+
if (keyedState) {
|
|
390
|
+
const key = keyFn(item);
|
|
391
|
+
const itemSig = signal(item);
|
|
392
|
+
accessor = itemSig;
|
|
393
|
+
keyedState.set(key, { itemSig });
|
|
394
|
+
} else {
|
|
395
|
+
accessor = item; // raw mode: pass item directly
|
|
396
|
+
}
|
|
397
|
+
const node = createRoot(dispose => {
|
|
398
|
+
disposeFns[idx] = dispose;
|
|
399
|
+
return mapFn(accessor, idx);
|
|
400
|
+
});
|
|
401
|
+
mappedNodes[i] = node;
|
|
402
|
+
frag.appendChild(node);
|
|
403
|
+
}
|
|
404
|
+
parent.insertBefore(frag, endMarker);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// --- Common prefix: skip matching keys at the start ---
|
|
409
|
+
let start = 0;
|
|
410
|
+
const minLen = Math.min(oldLen, newLen);
|
|
411
|
+
while (start < minLen) {
|
|
412
|
+
// Fast path: same reference → same key, no update needed
|
|
413
|
+
if (oldItems[start] === newItems[start]) { start++; continue; }
|
|
414
|
+
const oldKey = keyFn(oldItems[start]);
|
|
415
|
+
const newKey = keyFn(newItems[start]);
|
|
416
|
+
if (oldKey !== newKey) break;
|
|
417
|
+
// Key matches but reference changed — update signal (non-raw mode only)
|
|
418
|
+
if (keyedState) keyedState.get(oldKey).itemSig.set(newItems[start]);
|
|
419
|
+
start++;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- Common suffix: skip matching keys at the end ---
|
|
423
|
+
let oldEnd = oldLen - 1;
|
|
424
|
+
let newEnd = newLen - 1;
|
|
425
|
+
while (oldEnd >= start && newEnd >= start) {
|
|
426
|
+
if (oldItems[oldEnd] === newItems[newEnd]) { oldEnd--; newEnd--; continue; }
|
|
427
|
+
const oldKey = keyFn(oldItems[oldEnd]);
|
|
428
|
+
const newKey = keyFn(newItems[newEnd]);
|
|
429
|
+
if (oldKey !== newKey) break;
|
|
430
|
+
if (keyedState) keyedState.get(oldKey).itemSig.set(newItems[newEnd]);
|
|
431
|
+
oldEnd--;
|
|
432
|
+
newEnd--;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// If everything matched, nothing to do
|
|
436
|
+
if (start > oldEnd && start > newEnd) {
|
|
437
|
+
// Just copy existing mappings to output
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Copy prefix/suffix into output arrays
|
|
442
|
+
const newMapped = new Array(newLen);
|
|
443
|
+
const newDispose = new Array(newLen);
|
|
444
|
+
for (let i = 0; i < start; i++) {
|
|
445
|
+
newMapped[i] = mappedNodes[i];
|
|
446
|
+
newDispose[i] = disposeFns[i];
|
|
447
|
+
}
|
|
448
|
+
for (let i = newEnd + 1; i < newLen; i++) {
|
|
449
|
+
const oldI = oldEnd + 1 + (i - newEnd - 1);
|
|
450
|
+
newMapped[i] = mappedNodes[oldI];
|
|
451
|
+
newDispose[i] = disposeFns[oldI];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const midNewLen = newEnd - start + 1;
|
|
455
|
+
const midOldLen = oldEnd - start + 1;
|
|
456
|
+
|
|
457
|
+
// --- Only additions in middle ---
|
|
458
|
+
if (midOldLen === 0) {
|
|
459
|
+
const marker = newEnd + 1 < newLen && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
|
|
460
|
+
const frag = document.createDocumentFragment();
|
|
461
|
+
for (let i = start; i <= newEnd; i++) {
|
|
462
|
+
const item = newItems[i];
|
|
463
|
+
const idx = i;
|
|
464
|
+
let accessor;
|
|
465
|
+
if (keyedState) {
|
|
466
|
+
const key = keyFn(item);
|
|
467
|
+
const itemSig = signal(item);
|
|
468
|
+
accessor = itemSig;
|
|
469
|
+
keyedState.set(key, { itemSig });
|
|
470
|
+
} else {
|
|
471
|
+
accessor = item;
|
|
472
|
+
}
|
|
473
|
+
newMapped[i] = createRoot(dispose => {
|
|
474
|
+
newDispose[idx] = dispose;
|
|
475
|
+
return mapFn(accessor, idx);
|
|
476
|
+
});
|
|
477
|
+
frag.appendChild(newMapped[i]);
|
|
478
|
+
}
|
|
479
|
+
parent.insertBefore(frag, marker);
|
|
480
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- Only removals in middle ---
|
|
485
|
+
if (midNewLen === 0) {
|
|
486
|
+
for (let i = start; i <= oldEnd; i++) {
|
|
487
|
+
disposeFns[i]?.();
|
|
488
|
+
if (mappedNodes[i]?.parentNode) parent.removeChild(mappedNodes[i]);
|
|
489
|
+
if (keyedState) keyedState.delete(keyFn(oldItems[i]));
|
|
490
|
+
}
|
|
491
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- General case: reconcile middle section ---
|
|
496
|
+
// Build old key → old index map for middle section only
|
|
497
|
+
const oldKeyMap = new Map();
|
|
498
|
+
for (let i = start; i <= oldEnd; i++) {
|
|
499
|
+
oldKeyMap.set(keyFn(oldItems[i]), i);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const oldIndices = new Int32Array(midNewLen);
|
|
503
|
+
oldIndices.fill(-1);
|
|
504
|
+
|
|
505
|
+
// Match by key
|
|
506
|
+
for (let i = start; i <= newEnd; i++) {
|
|
507
|
+
const key = keyFn(newItems[i]);
|
|
508
|
+
const oldIdx = oldKeyMap.get(key);
|
|
509
|
+
if (oldIdx !== undefined) {
|
|
510
|
+
oldKeyMap.delete(key);
|
|
511
|
+
newMapped[i] = mappedNodes[oldIdx];
|
|
512
|
+
newDispose[i] = disposeFns[oldIdx];
|
|
513
|
+
oldIndices[i - start] = oldIdx;
|
|
514
|
+
// Update item signal if reference changed (non-raw mode only)
|
|
515
|
+
if (keyedState && newItems[i] !== oldItems[oldIdx]) {
|
|
516
|
+
keyedState.get(key).itemSig.set(newItems[i]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Dispose removed items
|
|
522
|
+
for (const [key, oldIdx] of oldKeyMap) {
|
|
523
|
+
disposeFns[oldIdx]?.();
|
|
524
|
+
if (mappedNodes[oldIdx]?.parentNode) parent.removeChild(mappedNodes[oldIdx]);
|
|
525
|
+
if (keyedState) keyedState.delete(key);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Create new items
|
|
529
|
+
for (let i = start; i <= newEnd; i++) {
|
|
530
|
+
if (!newMapped[i]) {
|
|
531
|
+
const item = newItems[i];
|
|
532
|
+
const idx = i;
|
|
533
|
+
let accessor;
|
|
534
|
+
if (keyedState) {
|
|
535
|
+
const key = keyFn(item);
|
|
536
|
+
const itemSig = signal(item);
|
|
537
|
+
accessor = itemSig;
|
|
538
|
+
keyedState.set(key, { itemSig });
|
|
539
|
+
} else {
|
|
540
|
+
accessor = item;
|
|
541
|
+
}
|
|
542
|
+
newMapped[i] = createRoot(dispose => {
|
|
543
|
+
newDispose[idx] = dispose;
|
|
544
|
+
return mapFn(accessor, idx);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Position using LIS
|
|
550
|
+
// First check: are reused items already in order? (common for update-in-place)
|
|
551
|
+
let reusedCount = 0;
|
|
552
|
+
let alreadySorted = true;
|
|
553
|
+
let lastOldIdx = -1;
|
|
554
|
+
for (let i = 0; i < midNewLen; i++) {
|
|
555
|
+
if (oldIndices[i] !== -1) {
|
|
556
|
+
reusedCount++;
|
|
557
|
+
if (oldIndices[i] <= lastOldIdx) alreadySorted = false;
|
|
558
|
+
lastOldIdx = oldIndices[i];
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const inLIS = new Uint8Array(midNewLen);
|
|
563
|
+
|
|
564
|
+
if (alreadySorted) {
|
|
565
|
+
// All reused items are in order — mark all as in LIS (no moves needed)
|
|
566
|
+
for (let i = 0; i < midNewLen; i++) {
|
|
567
|
+
if (oldIndices[i] !== -1) inLIS[i] = 1;
|
|
568
|
+
}
|
|
569
|
+
} else if (reusedCount > 1) {
|
|
570
|
+
const seq = new Int32Array(reusedCount);
|
|
571
|
+
const seqToMid = new Int32Array(reusedCount);
|
|
572
|
+
let k = 0;
|
|
573
|
+
for (let i = 0; i < midNewLen; i++) {
|
|
574
|
+
if (oldIndices[i] !== -1) {
|
|
575
|
+
seq[k] = oldIndices[i];
|
|
576
|
+
seqToMid[k] = i;
|
|
577
|
+
k++;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const lisResult = _lis(seq, reusedCount);
|
|
581
|
+
for (let i = 0; i < lisResult.length; i++) {
|
|
582
|
+
inLIS[seqToMid[lisResult[i]]] = 1;
|
|
583
|
+
}
|
|
584
|
+
} else if (reusedCount === 1) {
|
|
585
|
+
for (let i = 0; i < midNewLen; i++) {
|
|
586
|
+
if (oldIndices[i] !== -1) { inLIS[i] = 1; break; }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Position: work backwards, insert items not in LIS
|
|
591
|
+
let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1]
|
|
592
|
+
? newMapped[newEnd + 1] : endMarker;
|
|
593
|
+
|
|
594
|
+
for (let i = newEnd; i >= start; i--) {
|
|
595
|
+
const mi = i - start;
|
|
596
|
+
if (oldIndices[mi] === -1 || !inLIS[mi]) {
|
|
597
|
+
parent.insertBefore(newMapped[i], nextSibling);
|
|
598
|
+
}
|
|
599
|
+
nextSibling = newMapped[i];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen) {
|
|
606
|
+
mappedNodes.length = newLen;
|
|
607
|
+
disposeFns.length = newLen;
|
|
608
|
+
for (let i = 0; i < newLen; i++) {
|
|
609
|
+
mappedNodes[i] = newMapped[i];
|
|
610
|
+
disposeFns[i] = newDispose[i];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// --- spread(el, props) ---
|
|
615
|
+
// Fine-grained prop effects. Function props create individual effects.
|
|
616
|
+
// Event props use direct assignment.
|
|
617
|
+
|
|
618
|
+
export function spread(el, props) {
|
|
619
|
+
for (const key in props) {
|
|
620
|
+
const value = props[key];
|
|
621
|
+
|
|
622
|
+
if (key.startsWith('on') && key.length > 2) {
|
|
623
|
+
// Event handler — direct assignment. Use $$name for delegated events.
|
|
624
|
+
const event = key.slice(2).toLowerCase();
|
|
625
|
+
el.addEventListener(event, value);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (typeof value === 'function' && !key.startsWith('on')) {
|
|
630
|
+
// Reactive prop — create micro-effect
|
|
631
|
+
if (key === 'class' || key === 'className') {
|
|
632
|
+
effect(() => { el.className = value() || ''; });
|
|
633
|
+
} else if (key === 'style' && typeof value() === 'object') {
|
|
634
|
+
effect(() => {
|
|
635
|
+
const styles = value();
|
|
636
|
+
for (const prop in styles) {
|
|
637
|
+
el.style[prop] = styles[prop] ?? '';
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
effect(() => { setPropDirect(el, key, value()); });
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
// Static prop
|
|
645
|
+
setPropDirect(el, key, value);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function setPropDirect(el, key, value) {
|
|
651
|
+
if (key === 'class' || key === 'className') {
|
|
652
|
+
el.className = value || '';
|
|
653
|
+
} else if (key === 'style') {
|
|
654
|
+
if (typeof value === 'string') {
|
|
655
|
+
el.style.cssText = value;
|
|
656
|
+
} else if (typeof value === 'object') {
|
|
657
|
+
for (const prop in value) {
|
|
658
|
+
el.style[prop] = value[prop] ?? '';
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} else if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
662
|
+
el.setAttribute(key, value);
|
|
663
|
+
} else if (typeof value === 'boolean') {
|
|
664
|
+
if (value) el.setAttribute(key, '');
|
|
665
|
+
else el.removeAttribute(key);
|
|
666
|
+
} else if (key in el) {
|
|
667
|
+
el[key] = value;
|
|
668
|
+
} else {
|
|
669
|
+
el.setAttribute(key, value);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// --- delegateEvents(eventNames) ---
|
|
674
|
+
// Event delegation: common events handled at document level.
|
|
675
|
+
// Handlers stored as el.$$click, el.$$input, etc.
|
|
676
|
+
// Single listener per event type on document — reduces listener count from N to 1.
|
|
677
|
+
|
|
678
|
+
const delegatedEvents = new Set();
|
|
679
|
+
|
|
680
|
+
export function delegateEvents(eventNames) {
|
|
681
|
+
for (const name of eventNames) {
|
|
682
|
+
if (delegatedEvents.has(name)) continue;
|
|
683
|
+
delegatedEvents.add(name);
|
|
684
|
+
|
|
685
|
+
document.addEventListener(name, (e) => {
|
|
686
|
+
let node = e.target;
|
|
687
|
+
const key = '$$' + name;
|
|
688
|
+
|
|
689
|
+
// Walk up the DOM tree looking for handlers
|
|
690
|
+
while (node) {
|
|
691
|
+
const handler = node[key];
|
|
692
|
+
if (handler) {
|
|
693
|
+
handler(e);
|
|
694
|
+
if (e.cancelBubble) return;
|
|
695
|
+
}
|
|
696
|
+
node = node.parentNode;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// --- addEventListener helper for non-delegated events ---
|
|
703
|
+
export function on(el, event, handler) {
|
|
704
|
+
el.addEventListener(event, handler);
|
|
705
|
+
return () => el.removeEventListener(event, handler);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- className helper for conditional classes ---
|
|
709
|
+
export function classList(el, classes) {
|
|
710
|
+
effect(() => {
|
|
711
|
+
for (const name in classes) {
|
|
712
|
+
const value = typeof classes[name] === 'function' ? classes[name]() : classes[name];
|
|
713
|
+
el.classList.toggle(name, !!value);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
}
|