kerfjs 0.4.2 → 0.5.1
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/CHANGELOG.md +19 -0
- package/README.md +27 -6
- package/dist/array-signal.d.ts +86 -0
- package/dist/array-signal.js +98 -0
- package/dist/array-signal.js.map +1 -0
- package/dist/chunk-FN2ID4QO.js +3 -0
- package/dist/chunk-FN2ID4QO.js.map +1 -0
- package/dist/{chunk-IZJIKRCE.js → chunk-GQGJFCWL.js} +3 -6
- package/dist/chunk-GQGJFCWL.js.map +1 -0
- package/dist/{chunk-WK5D3OO7.js → chunk-KFOSUCCC.js} +14 -3
- package/dist/chunk-KFOSUCCC.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +441 -77
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +682 -3
- package/dist/jsx-runtime.js +1 -1
- package/dist/testing.js +2 -1
- package/package.json +13 -3
- package/dist/chunk-IZJIKRCE.js.map +0 -1
- package/dist/chunk-WK5D3OO7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { isSafeHtml, listSafeHtml,
|
|
2
|
-
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-KFOSUCCC.js';
|
|
2
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-KFOSUCCC.js';
|
|
3
|
+
export { defineStore, resetAllStores } from './chunk-GQGJFCWL.js';
|
|
4
|
+
import { effect } from './chunk-FN2ID4QO.js';
|
|
5
|
+
export { batch, computed, effect, signal } from './chunk-FN2ID4QO.js';
|
|
5
6
|
|
|
6
7
|
// src/delegate.ts
|
|
7
8
|
var NON_BUBBLING = /* @__PURE__ */ new Set([
|
|
@@ -54,14 +55,87 @@ function delegateCapture(rootEl, type, selector, handler) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// src/each.ts
|
|
57
|
-
var
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
|
|
59
|
+
function isArraySignal(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
|
|
61
|
+
}
|
|
62
|
+
var context = null;
|
|
63
|
+
function _setRenderContext(c) {
|
|
64
|
+
context = c;
|
|
61
65
|
}
|
|
62
66
|
function each(items, render, key) {
|
|
63
|
-
|
|
67
|
+
if (isArraySignal(items) && context !== null) {
|
|
68
|
+
return eachGranular(items, render, key);
|
|
69
|
+
}
|
|
70
|
+
const snapshotItems = isArraySignal(items) ? items.value : items;
|
|
71
|
+
return eachSnapshot(snapshotItems, render, key);
|
|
72
|
+
}
|
|
73
|
+
function eachSnapshot(items, render, key) {
|
|
74
|
+
let id;
|
|
75
|
+
if (context !== null) {
|
|
76
|
+
id = String(context.counter++);
|
|
77
|
+
} else {
|
|
78
|
+
id = "orphan";
|
|
79
|
+
}
|
|
80
|
+
return eachSnapshotById(items, render, key, id);
|
|
81
|
+
}
|
|
82
|
+
function eachGranular(sig, render, key) {
|
|
83
|
+
const ctx = context;
|
|
84
|
+
const id = String(ctx.counter++);
|
|
85
|
+
const previousBindingCount = ctx.bindingCounts.get(id);
|
|
86
|
+
const patches = sig._consumePatches();
|
|
87
|
+
const snapshot = sig.value;
|
|
88
|
+
if (previousBindingCount === void 0 || patches.length === 0) {
|
|
89
|
+
return eachSnapshotById(snapshot, render, key, id);
|
|
90
|
+
}
|
|
91
|
+
let netDelta = 0;
|
|
92
|
+
for (const p of patches) {
|
|
93
|
+
if (p.type === "insert") netDelta += 1;
|
|
94
|
+
else if (p.type === "remove") netDelta -= 1;
|
|
95
|
+
else if (p.type === "replace") {
|
|
96
|
+
return eachSnapshotById(snapshot, render, key, id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (previousBindingCount + netDelta !== snapshot.length) {
|
|
100
|
+
return eachSnapshotById(snapshot, render, key, id);
|
|
101
|
+
}
|
|
102
|
+
const renderFnInternal = (item, index) => {
|
|
103
|
+
const out = render(item, index);
|
|
104
|
+
return isSafeHtml(out) ? out.toString() : out;
|
|
105
|
+
};
|
|
106
|
+
const internalPatches = new Array(patches.length);
|
|
107
|
+
try {
|
|
108
|
+
for (let i = 0; i < patches.length; i++) {
|
|
109
|
+
const p = patches[i];
|
|
110
|
+
if (p.type === "insert" || p.type === "update") {
|
|
111
|
+
internalPatches[i] = {
|
|
112
|
+
type: p.type,
|
|
113
|
+
index: p.index,
|
|
114
|
+
item: p.item,
|
|
115
|
+
html: renderFnInternal(p.item, p.index)
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
internalPatches[i] = p;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
ctx.bindingCounts.delete(id);
|
|
123
|
+
return eachSnapshotById(snapshot, render, key, id);
|
|
124
|
+
}
|
|
125
|
+
return granularListSafeHtml(id, [], internalPatches);
|
|
126
|
+
}
|
|
127
|
+
function eachSnapshotById(items, render, key, id) {
|
|
128
|
+
let cache = null;
|
|
129
|
+
if (context !== null) {
|
|
130
|
+
let c = context.caches.get(id);
|
|
131
|
+
if (c === void 0) {
|
|
132
|
+
c = /* @__PURE__ */ new WeakMap();
|
|
133
|
+
context.caches.set(id, c);
|
|
134
|
+
}
|
|
135
|
+
cache = c;
|
|
136
|
+
}
|
|
64
137
|
const segItems = new Array(items.length);
|
|
138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65
139
|
for (let i = 0; i < items.length; i++) {
|
|
66
140
|
const item = items[i];
|
|
67
141
|
if (typeof item !== "object" || item === null) {
|
|
@@ -69,15 +143,21 @@ function each(items, render, key) {
|
|
|
69
143
|
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${i}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
|
|
70
144
|
);
|
|
71
145
|
}
|
|
146
|
+
if (seen.has(item)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
seen.add(item);
|
|
72
152
|
const k = key ? key(item, i) : void 0;
|
|
73
|
-
const cached = ROW_CACHE.get(item);
|
|
74
153
|
let html;
|
|
154
|
+
const cached = cache !== null ? cache.get(item) : void 0;
|
|
75
155
|
if (cached !== void 0 && cached.key === k) {
|
|
76
156
|
html = cached.html;
|
|
77
157
|
} else {
|
|
78
158
|
const out = render(item, i);
|
|
79
159
|
html = isSafeHtml(out) ? out.toString() : out;
|
|
80
|
-
|
|
160
|
+
if (cache !== null) cache.set(item, { key: k, html });
|
|
81
161
|
}
|
|
82
162
|
segItems[i] = { ref: item, cacheKey: k, html };
|
|
83
163
|
}
|
|
@@ -99,16 +179,23 @@ function getNodeKey(node) {
|
|
|
99
179
|
}
|
|
100
180
|
return void 0;
|
|
101
181
|
}
|
|
102
|
-
function diff(liveRoot, templateRoot,
|
|
103
|
-
diffChildren(liveRoot, templateRoot,
|
|
182
|
+
function diff(liveRoot, templateRoot, ownedItems) {
|
|
183
|
+
diffChildren(liveRoot, templateRoot, ownedItems);
|
|
184
|
+
}
|
|
185
|
+
function skipOwned(node, ownedItems) {
|
|
186
|
+
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
|
|
187
|
+
node = node.nextSibling;
|
|
188
|
+
}
|
|
189
|
+
return node;
|
|
104
190
|
}
|
|
105
|
-
function diffChildren(fromParent, toParent,
|
|
191
|
+
function diffChildren(fromParent, toParent, ownedItems) {
|
|
106
192
|
const keyed = /* @__PURE__ */ new Map();
|
|
107
193
|
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
|
|
194
|
+
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
|
|
108
195
|
const k = getNodeKey(c);
|
|
109
196
|
if (k !== void 0) keyed.set(k, c);
|
|
110
197
|
}
|
|
111
|
-
let fromChild = fromParent.firstChild;
|
|
198
|
+
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
|
|
112
199
|
let toChild = toParent.firstChild;
|
|
113
200
|
while (toChild !== null) {
|
|
114
201
|
const toNext = toChild.nextSibling;
|
|
@@ -120,15 +207,15 @@ function diffChildren(fromParent, toParent, listParents) {
|
|
|
120
207
|
if (matched !== fromChild) {
|
|
121
208
|
fromParent.insertBefore(matched, fromChild);
|
|
122
209
|
} else {
|
|
123
|
-
fromChild = fromChild.nextSibling;
|
|
210
|
+
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
124
211
|
}
|
|
125
212
|
}
|
|
126
213
|
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0)) {
|
|
127
214
|
matched = fromChild;
|
|
128
|
-
fromChild = fromChild.nextSibling;
|
|
215
|
+
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
129
216
|
}
|
|
130
217
|
if (matched !== null) {
|
|
131
|
-
morphNode(matched, toChild,
|
|
218
|
+
morphNode(matched, toChild, ownedItems);
|
|
132
219
|
} else {
|
|
133
220
|
const cloned = toChild.cloneNode(true);
|
|
134
221
|
fromParent.insertBefore(cloned, fromChild);
|
|
@@ -137,13 +224,15 @@ function diffChildren(fromParent, toParent, listParents) {
|
|
|
137
224
|
}
|
|
138
225
|
while (fromChild !== null) {
|
|
139
226
|
const next = fromChild.nextSibling;
|
|
140
|
-
|
|
227
|
+
if (fromChild.nodeType !== ELEMENT_NODE || !ownedItems.has(fromChild)) {
|
|
228
|
+
fromParent.removeChild(fromChild);
|
|
229
|
+
}
|
|
141
230
|
fromChild = next;
|
|
142
231
|
}
|
|
143
232
|
}
|
|
144
|
-
function morphNode(fromNode, toNode,
|
|
233
|
+
function morphNode(fromNode, toNode, ownedItems) {
|
|
145
234
|
if (fromNode.nodeType === ELEMENT_NODE) {
|
|
146
|
-
morphElement(fromNode, toNode,
|
|
235
|
+
morphElement(fromNode, toNode, ownedItems);
|
|
147
236
|
return;
|
|
148
237
|
}
|
|
149
238
|
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
|
|
@@ -152,7 +241,7 @@ function morphNode(fromNode, toNode, listParents) {
|
|
|
152
241
|
if (fromText.data !== toText.data) fromText.data = toText.data;
|
|
153
242
|
}
|
|
154
243
|
}
|
|
155
|
-
function morphElement(fromEl, toEl,
|
|
244
|
+
function morphElement(fromEl, toEl, ownedItems) {
|
|
156
245
|
if (fromEl.tagName !== toEl.tagName) {
|
|
157
246
|
const replacement = toEl.cloneNode(true);
|
|
158
247
|
fromEl.parentNode?.replaceChild(replacement, fromEl);
|
|
@@ -166,8 +255,10 @@ function morphElement(fromEl, toEl, listParents) {
|
|
|
166
255
|
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
|
|
167
256
|
}
|
|
168
257
|
morphAttributes(fromEl, toEl);
|
|
169
|
-
|
|
170
|
-
|
|
258
|
+
diffChildren(fromEl, toEl, ownedItems);
|
|
259
|
+
}
|
|
260
|
+
function isUserAgentOwnedAttr(tagName, name) {
|
|
261
|
+
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
|
|
171
262
|
}
|
|
172
263
|
function morphAttributes(fromEl, toEl) {
|
|
173
264
|
const toAttrs = toEl.attributes;
|
|
@@ -185,13 +276,14 @@ function morphAttributes(fromEl, toEl) {
|
|
|
185
276
|
}
|
|
186
277
|
}
|
|
187
278
|
const fromAttrs = fromEl.attributes;
|
|
279
|
+
const fromTag = fromEl.tagName;
|
|
188
280
|
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
|
189
281
|
const attr = fromAttrs[i];
|
|
190
282
|
const ns = attr.namespaceURI;
|
|
191
283
|
const name = attr.localName;
|
|
192
284
|
if (ns !== null) {
|
|
193
285
|
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
|
|
194
|
-
} else if (!toEl.hasAttribute(name)) {
|
|
286
|
+
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
|
|
195
287
|
fromEl.removeAttribute(name);
|
|
196
288
|
}
|
|
197
289
|
}
|
|
@@ -216,6 +308,14 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
216
308
|
}
|
|
217
309
|
}
|
|
218
310
|
|
|
311
|
+
// src/list-binding.ts
|
|
312
|
+
function endAnchor(binding) {
|
|
313
|
+
if (binding.items.length > 0) {
|
|
314
|
+
return binding.items[binding.items.length - 1].node.nextElementSibling;
|
|
315
|
+
}
|
|
316
|
+
return binding.marker.nextElementSibling;
|
|
317
|
+
}
|
|
318
|
+
|
|
219
319
|
// src/list-reconcile-focus.ts
|
|
220
320
|
function captureFocus(liveParent) {
|
|
221
321
|
const active = document.activeElement;
|
|
@@ -245,13 +345,212 @@ function restoreFocus(snap) {
|
|
|
245
345
|
}
|
|
246
346
|
}
|
|
247
347
|
|
|
248
|
-
// src/
|
|
348
|
+
// src/utils/rowContract.ts
|
|
349
|
+
var ROW_HTML_SNIPPET_MAX = 120;
|
|
350
|
+
function truncateRowHtml(html) {
|
|
351
|
+
return html.length > ROW_HTML_SNIPPET_MAX ? html.slice(0, ROW_HTML_SNIPPET_MAX) + "\u2026" : html;
|
|
352
|
+
}
|
|
353
|
+
function parseRowTemplate(html) {
|
|
354
|
+
const tpl = document.createElement("template");
|
|
355
|
+
tpl.innerHTML = html;
|
|
356
|
+
return { tpl, count: tpl.content.children.length };
|
|
357
|
+
}
|
|
358
|
+
function rowContractError(index, html) {
|
|
359
|
+
const { count } = parseRowTemplate(html);
|
|
360
|
+
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
361
|
+
return new Error(
|
|
362
|
+
`each(): row render at index ${index} ${reason}. Each item's render must return exactly one element \u2014 wrap multiple roots in a single parent (e.g. <li>...</li>). Got HTML: ${JSON.stringify(truncateRowHtml(html))}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/list-reconcile-granular.ts
|
|
367
|
+
function reconcileGranular(binding, patches) {
|
|
368
|
+
const { liveParent } = binding;
|
|
369
|
+
const items = binding.items;
|
|
370
|
+
const focusSnap = captureFocus(liveParent);
|
|
371
|
+
let i = 0;
|
|
372
|
+
while (i < patches.length) {
|
|
373
|
+
const patch = patches[i];
|
|
374
|
+
if (patch.type === "replace") {
|
|
375
|
+
i += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (patch.type === "update") {
|
|
379
|
+
let runEnd = i + 1;
|
|
380
|
+
while (runEnd < patches.length && patches[runEnd].type === "update") {
|
|
381
|
+
runEnd += 1;
|
|
382
|
+
}
|
|
383
|
+
const runLen = runEnd - i;
|
|
384
|
+
if (runLen === 1) {
|
|
385
|
+
applySingleUpdate(liveParent, items, patch);
|
|
386
|
+
} else {
|
|
387
|
+
applyBulkUpdate(liveParent, items, patches, i, runEnd);
|
|
388
|
+
}
|
|
389
|
+
i = runEnd;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (patch.type === "insert") {
|
|
393
|
+
let runEnd = i + 1;
|
|
394
|
+
while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
|
|
395
|
+
runEnd += 1;
|
|
396
|
+
}
|
|
397
|
+
const runLen = runEnd - i;
|
|
398
|
+
if (runLen === 1) {
|
|
399
|
+
applySingleInsert(liveParent, items, patch, endAnchor(binding));
|
|
400
|
+
} else {
|
|
401
|
+
applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
|
|
402
|
+
}
|
|
403
|
+
i = runEnd;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (patch.type === "remove") {
|
|
407
|
+
const entry = items[patch.index];
|
|
408
|
+
liveParent.removeChild(entry.node);
|
|
409
|
+
items.splice(patch.index, 1);
|
|
410
|
+
i += 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (patch.type === "move") {
|
|
414
|
+
const moved = items[patch.from];
|
|
415
|
+
let anchorIdx = patch.to;
|
|
416
|
+
if (patch.from < patch.to) anchorIdx += 1;
|
|
417
|
+
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
|
|
418
|
+
liveParent.insertBefore(moved.node, anchor);
|
|
419
|
+
items.splice(patch.from, 1);
|
|
420
|
+
items.splice(patch.to, 0, moved);
|
|
421
|
+
i += 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
426
|
+
}
|
|
427
|
+
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
428
|
+
const { html } = patch;
|
|
429
|
+
const newNode = parseSingleRow(html);
|
|
430
|
+
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
|
|
431
|
+
liveParent.insertBefore(newNode, anchor);
|
|
432
|
+
items.splice(patch.index, 0, {
|
|
433
|
+
ref: patch.item,
|
|
434
|
+
cacheKey: void 0,
|
|
435
|
+
html,
|
|
436
|
+
node: newNode
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function applySingleUpdate(liveParent, items, patch) {
|
|
440
|
+
const { html } = patch;
|
|
441
|
+
const oldEntry = items[patch.index];
|
|
442
|
+
if (html === oldEntry.html) return;
|
|
443
|
+
const newNode = parseSingleRow(html);
|
|
444
|
+
liveParent.replaceChild(newNode, oldEntry.node);
|
|
445
|
+
items[patch.index] = { ref: patch.item, cacheKey: void 0, html, node: newNode };
|
|
446
|
+
}
|
|
447
|
+
function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
448
|
+
const changes = [];
|
|
449
|
+
for (let k = start; k < end; k++) {
|
|
450
|
+
const p = patches[k];
|
|
451
|
+
if (p.html !== items[p.index].html) {
|
|
452
|
+
changes.push({ patchIdx: k, html: p.html });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (changes.length === 0) return;
|
|
456
|
+
const { tpl, count } = parseRowTemplate(changes.map((c) => c.html).join(""));
|
|
457
|
+
if (count !== changes.length) {
|
|
458
|
+
throw findOffendingChange(patches, changes);
|
|
459
|
+
}
|
|
460
|
+
const newNodes = new Array(changes.length);
|
|
461
|
+
let child = tpl.content.firstElementChild;
|
|
462
|
+
for (let k = 0; k < newNodes.length; k++) {
|
|
463
|
+
newNodes[k] = child;
|
|
464
|
+
child = child.nextElementSibling;
|
|
465
|
+
}
|
|
466
|
+
for (let k = 0; k < changes.length; k++) {
|
|
467
|
+
const c = changes[k];
|
|
468
|
+
const p = patches[c.patchIdx];
|
|
469
|
+
const oldEntry = items[p.index];
|
|
470
|
+
liveParent.replaceChild(newNodes[k], oldEntry.node);
|
|
471
|
+
items[p.index] = { ref: p.item, cacheKey: void 0, html: c.html, node: newNodes[k] };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
475
|
+
const startIdx = patches[start].index;
|
|
476
|
+
const htmls = new Array(end - start);
|
|
477
|
+
for (let k = start; k < end; k++) {
|
|
478
|
+
const p = patches[k];
|
|
479
|
+
htmls[k - start] = p.html;
|
|
480
|
+
}
|
|
481
|
+
const { tpl, count } = parseRowTemplate(htmls.join(""));
|
|
482
|
+
if (count !== htmls.length) {
|
|
483
|
+
throw findOffendingInsert(patches, start, htmls);
|
|
484
|
+
}
|
|
485
|
+
const newNodes = new Array(end - start);
|
|
486
|
+
let child = tpl.content.firstElementChild;
|
|
487
|
+
for (let k = 0; k < newNodes.length; k++) {
|
|
488
|
+
newNodes[k] = child;
|
|
489
|
+
child = child.nextElementSibling;
|
|
490
|
+
}
|
|
491
|
+
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
|
|
492
|
+
liveParent.insertBefore(tpl.content, anchor);
|
|
493
|
+
const newEntries = new Array(end - start);
|
|
494
|
+
for (let k = 0; k < newEntries.length; k++) {
|
|
495
|
+
const p = patches[start + k];
|
|
496
|
+
newEntries[k] = {
|
|
497
|
+
ref: p.item,
|
|
498
|
+
cacheKey: void 0,
|
|
499
|
+
html: htmls[k],
|
|
500
|
+
node: newNodes[k]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
items.splice(startIdx, 0, ...newEntries);
|
|
504
|
+
}
|
|
505
|
+
function parseSingleRow(html) {
|
|
506
|
+
const { tpl, count } = parseRowTemplate(html);
|
|
507
|
+
if (count !== 1) {
|
|
508
|
+
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
509
|
+
throw new Error(
|
|
510
|
+
`each() granular reconcile: row render ${reason}. Each item's render must return exactly one element. Got HTML: ${JSON.stringify(truncateRowHtml(html))}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return tpl.content.firstElementChild;
|
|
514
|
+
}
|
|
515
|
+
function findOffendingInsert(patches, start, htmls) {
|
|
516
|
+
for (let i = 0; i < htmls.length; i++) {
|
|
517
|
+
if (parseRowTemplate(htmls[i]).count !== 1) {
|
|
518
|
+
const p = patches[start + i];
|
|
519
|
+
return rowContractError(p.index, htmls[i]);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
|
|
523
|
+
}
|
|
524
|
+
function findOffendingChange(patches, changes) {
|
|
525
|
+
for (const c of changes) {
|
|
526
|
+
if (parseRowTemplate(c.html).count !== 1) {
|
|
527
|
+
const p = patches[c.patchIdx];
|
|
528
|
+
return rowContractError(p.index, c.html);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/list-reconcile-snapshot.ts
|
|
535
|
+
function reconcileSnapshot(binding, listSeg) {
|
|
536
|
+
const { liveParent } = binding;
|
|
537
|
+
const { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
|
|
538
|
+
if (replacedNodes.length === 0 && freshIndices.length === 0 && isInOrder(prevIdx)) {
|
|
539
|
+
binding.items = newRecord;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const tailAnchor = endAnchor(binding);
|
|
543
|
+
buildFreshNodes(newRecord, freshIndices, freshHtmls);
|
|
544
|
+
const focusSnap = captureFocus(liveParent);
|
|
545
|
+
removeOldNodes(liveParent, replacedNodes);
|
|
546
|
+
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
|
|
547
|
+
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
548
|
+
binding.items = newRecord;
|
|
549
|
+
}
|
|
249
550
|
function classifyItems(oldItems, listSeg) {
|
|
250
551
|
const oldByRef = /* @__PURE__ */ new Map();
|
|
251
|
-
const oldIndex = /* @__PURE__ */ new Map();
|
|
252
552
|
for (let i = 0; i < oldItems.length; i++) {
|
|
253
|
-
oldByRef.set(oldItems[i].ref, oldItems[i]);
|
|
254
|
-
oldIndex.set(oldItems[i].ref, i);
|
|
553
|
+
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
|
|
255
554
|
}
|
|
256
555
|
const newRecord = new Array(listSeg.items.length);
|
|
257
556
|
const prevIdx = new Array(listSeg.items.length);
|
|
@@ -263,12 +562,12 @@ function classifyItems(oldItems, listSeg) {
|
|
|
263
562
|
const oi = oldByRef.get(ni.ref);
|
|
264
563
|
if (oi !== void 0) {
|
|
265
564
|
oldByRef.delete(ni.ref);
|
|
266
|
-
if (oi.html === ni.html) {
|
|
267
|
-
newRecord[i] = oi;
|
|
268
|
-
prevIdx[i] =
|
|
565
|
+
if (oi[0].html === ni.html) {
|
|
566
|
+
newRecord[i] = oi[0];
|
|
567
|
+
prevIdx[i] = oi[1];
|
|
269
568
|
continue;
|
|
270
569
|
}
|
|
271
|
-
replacedNodes.push(oi.node);
|
|
570
|
+
replacedNodes.push(oi[0].node);
|
|
272
571
|
}
|
|
273
572
|
newRecord[i] = {
|
|
274
573
|
ref: ni.ref,
|
|
@@ -280,32 +579,37 @@ function classifyItems(oldItems, listSeg) {
|
|
|
280
579
|
freshIndices.push(i);
|
|
281
580
|
freshHtmls.push(ni.html);
|
|
282
581
|
}
|
|
283
|
-
for (const [, orphan] of oldByRef) replacedNodes.push(orphan.node);
|
|
582
|
+
for (const [, orphan] of oldByRef) replacedNodes.push(orphan[0].node);
|
|
284
583
|
return { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls };
|
|
285
584
|
}
|
|
286
585
|
function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
287
586
|
if (freshHtmls.length === 0) return;
|
|
288
|
-
const tpl =
|
|
289
|
-
|
|
587
|
+
const { tpl, count } = parseRowTemplate(freshHtmls.join(""));
|
|
588
|
+
if (count !== freshHtmls.length) {
|
|
589
|
+
throw findOffendingRow(newRecord, freshIndices, freshHtmls);
|
|
590
|
+
}
|
|
290
591
|
let node = tpl.content.firstElementChild;
|
|
291
592
|
for (const idx of freshIndices) {
|
|
292
|
-
if (node === null) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
`each(): row render produced no top-level element. Each item's render must return exactly one element. Got HTML: ${newRecord[idx].html.slice(0, 120)}`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
593
|
const next = node.nextElementSibling;
|
|
298
594
|
newRecord[idx].node = node;
|
|
299
595
|
node = next;
|
|
300
596
|
}
|
|
301
597
|
}
|
|
598
|
+
function findOffendingRow(newRecord, freshIndices, freshHtmls) {
|
|
599
|
+
for (let i = 0; i < freshHtmls.length; i++) {
|
|
600
|
+
if (parseRowTemplate(freshHtmls[i]).count !== 1) {
|
|
601
|
+
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
|
|
605
|
+
}
|
|
302
606
|
function removeOldNodes(liveParent, replacedNodes) {
|
|
303
607
|
for (const node of replacedNodes) {
|
|
304
608
|
if (node.parentElement === liveParent) liveParent.removeChild(node);
|
|
305
609
|
}
|
|
306
610
|
}
|
|
307
|
-
function applyMoves(liveParent, newRecord, prevIdx, stable) {
|
|
308
|
-
let nextSibling =
|
|
611
|
+
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
|
|
612
|
+
let nextSibling = tailAnchor;
|
|
309
613
|
for (let i = newRecord.length - 1; i >= 0; i--) {
|
|
310
614
|
const node = newRecord[i].node;
|
|
311
615
|
if (prevIdx[i] === -1 || !stable.has(i)) {
|
|
@@ -343,15 +647,20 @@ function lis(arr) {
|
|
|
343
647
|
}
|
|
344
648
|
return out;
|
|
345
649
|
}
|
|
650
|
+
function isInOrder(prevIdx) {
|
|
651
|
+
for (let i = 0; i < prevIdx.length; i++) {
|
|
652
|
+
if (prevIdx[i] !== i) return false;
|
|
653
|
+
}
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/list-reconcile.ts
|
|
346
658
|
function reconcileList(binding, listSeg) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx));
|
|
353
|
-
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
354
|
-
binding.items = newRecord;
|
|
659
|
+
if (listSeg.patches !== void 0 && binding.items.length > 0) {
|
|
660
|
+
reconcileGranular(binding, listSeg.patches);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
reconcileSnapshot(binding, listSeg);
|
|
355
664
|
}
|
|
356
665
|
|
|
357
666
|
// src/mount.ts
|
|
@@ -363,58 +672,113 @@ function mount(rootEl, render) {
|
|
|
363
672
|
);
|
|
364
673
|
}
|
|
365
674
|
const bindings = /* @__PURE__ */ new Map();
|
|
366
|
-
const
|
|
675
|
+
const renderCtx = {
|
|
676
|
+
counter: 0,
|
|
677
|
+
caches: /* @__PURE__ */ new Map(),
|
|
678
|
+
bindingCounts: /* @__PURE__ */ new Map()
|
|
679
|
+
};
|
|
367
680
|
let isFirst = true;
|
|
681
|
+
let prevStaticHtml = "";
|
|
368
682
|
return effect(() => {
|
|
369
|
-
counter
|
|
370
|
-
|
|
683
|
+
renderCtx.counter = 0;
|
|
684
|
+
_setRenderContext(renderCtx);
|
|
371
685
|
let result;
|
|
372
686
|
try {
|
|
373
687
|
result = render();
|
|
374
688
|
} finally {
|
|
375
|
-
|
|
689
|
+
_setRenderContext(null);
|
|
376
690
|
}
|
|
377
|
-
const segment = isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: result };
|
|
691
|
+
const segment = isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
|
|
378
692
|
if (isFirst) {
|
|
379
|
-
rootEl
|
|
380
|
-
|
|
693
|
+
runFirstRender(rootEl, segment, bindings);
|
|
694
|
+
prevStaticHtml = flattenWithoutListItems(segment);
|
|
381
695
|
isFirst = false;
|
|
382
696
|
} else {
|
|
383
|
-
|
|
384
|
-
template.innerHTML = flattenWithoutListItems(segment);
|
|
385
|
-
const listParents = /* @__PURE__ */ new Set();
|
|
386
|
-
for (const b of bindings.values()) listParents.add(b.liveParent);
|
|
387
|
-
diff(rootEl, template, listParents);
|
|
388
|
-
bindListsFromMarkers(rootEl, segment, bindings);
|
|
697
|
+
prevStaticHtml = runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml);
|
|
389
698
|
}
|
|
390
699
|
for (const listSeg of collectLists(segment).values()) {
|
|
391
700
|
const binding = bindings.get(listSeg.id);
|
|
392
701
|
reconcileList(binding, listSeg);
|
|
702
|
+
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
393
703
|
}
|
|
394
704
|
});
|
|
395
705
|
}
|
|
396
|
-
function
|
|
706
|
+
function runFirstRender(rootEl, segment, bindings) {
|
|
707
|
+
rootEl.innerHTML = flatten(segment, true);
|
|
708
|
+
bindListsFromMarkers(rootEl, segment, bindings, true);
|
|
709
|
+
}
|
|
710
|
+
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml) {
|
|
711
|
+
const currentStaticHtml = flattenWithoutListItems(segment);
|
|
712
|
+
if (currentStaticHtml === prevStaticHtml) {
|
|
713
|
+
return prevStaticHtml;
|
|
714
|
+
}
|
|
715
|
+
cleanupOrphanBindings(segment, bindings, renderCtx);
|
|
716
|
+
const template = rootEl.cloneNode(false);
|
|
717
|
+
template.innerHTML = currentStaticHtml;
|
|
718
|
+
diff(rootEl, template, collectOwnedItems(bindings));
|
|
719
|
+
bindListsFromMarkers(rootEl, segment, bindings, false);
|
|
720
|
+
return currentStaticHtml;
|
|
721
|
+
}
|
|
722
|
+
function coerceRenderResult(result) {
|
|
723
|
+
if (result === null || result === void 0) return "";
|
|
724
|
+
if (result === false || result === true) return "";
|
|
725
|
+
return String(result);
|
|
726
|
+
}
|
|
727
|
+
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
397
728
|
const lists = collectLists(segment);
|
|
398
729
|
const found = [];
|
|
399
730
|
collectComments(rootEl, found);
|
|
400
731
|
for (const marker of found) {
|
|
401
732
|
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
|
|
402
733
|
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
|
|
734
|
+
if (bindings.has(id)) continue;
|
|
403
735
|
const listSeg = lists.get(id);
|
|
404
736
|
const liveParent = marker.parentElement;
|
|
405
737
|
const items = [];
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
items.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
738
|
+
if (inlinedItems) {
|
|
739
|
+
let next = marker.nextElementSibling;
|
|
740
|
+
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
|
|
741
|
+
validateInlinedRowMatch(listSeg.items[i].html, i, next);
|
|
742
|
+
items.push({
|
|
743
|
+
ref: listSeg.items[i].ref,
|
|
744
|
+
cacheKey: listSeg.items[i].cacheKey,
|
|
745
|
+
html: listSeg.items[i].html,
|
|
746
|
+
node: next
|
|
747
|
+
});
|
|
748
|
+
next = next.nextElementSibling;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
bindings.set(id, { liveParent, items, marker });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function validateInlinedRowMatch(expectedHtml, index, boundEl) {
|
|
755
|
+
if (boundEl.outerHTML === expectedHtml) return;
|
|
756
|
+
const { count } = parseRowTemplate(expectedHtml);
|
|
757
|
+
if (count === 1) return;
|
|
758
|
+
throw rowContractError(index, expectedHtml);
|
|
759
|
+
}
|
|
760
|
+
function collectOwnedItems(bindings) {
|
|
761
|
+
const owned = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const b of bindings.values()) {
|
|
763
|
+
for (const item of b.items) owned.add(item.node);
|
|
764
|
+
}
|
|
765
|
+
return owned;
|
|
766
|
+
}
|
|
767
|
+
function cleanupOrphanBindings(segment, bindings, renderCtx) {
|
|
768
|
+
const liveIds = collectLists(segment);
|
|
769
|
+
for (const [id, binding] of bindings) {
|
|
770
|
+
if (liveIds.has(id)) continue;
|
|
771
|
+
for (const item of binding.items) {
|
|
772
|
+
if (item.node.parentElement !== null) {
|
|
773
|
+
item.node.parentElement.removeChild(item.node);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (binding.marker.parentElement !== null) {
|
|
777
|
+
binding.marker.parentElement.removeChild(binding.marker);
|
|
778
|
+
}
|
|
779
|
+
bindings.delete(id);
|
|
780
|
+
renderCtx.bindingCounts.delete(id);
|
|
781
|
+
renderCtx.caches.delete(id);
|
|
418
782
|
}
|
|
419
783
|
}
|
|
420
784
|
function collectComments(node, out) {
|