kerfjs 0.3.1 → 0.5.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/CHANGELOG.md +45 -0
- package/README.md +86 -54
- 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-WYJTMERY.js} +14 -3
- package/dist/chunk-WYJTMERY.js.map +1 -0
- package/dist/index.d.ts +25 -12
- package/dist/index.js +492 -77
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +674 -3
- package/dist/jsx-runtime.js +1 -1
- package/dist/testing.js +2 -1
- package/package.json +17 -5
- package/dist/chunk-IZJIKRCE.js.map +0 -1
- package/dist/chunk-WK5D3OO7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { isSafeHtml, listSafeHtml,
|
|
2
|
-
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-WYJTMERY.js';
|
|
2
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-WYJTMERY.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
|
|
8
|
+
var NON_BUBBLING = /* @__PURE__ */ new Set([
|
|
9
|
+
"focus",
|
|
10
|
+
"blur",
|
|
11
|
+
"scroll",
|
|
12
|
+
"load",
|
|
13
|
+
"error",
|
|
14
|
+
"mouseenter",
|
|
15
|
+
"mouseleave"
|
|
16
|
+
]);
|
|
7
17
|
function assertValidSelector(selector, fn) {
|
|
8
18
|
try {
|
|
9
19
|
document.createElement("div").matches(selector);
|
|
@@ -23,9 +33,10 @@ function delegate(rootEl, type, selector, handler) {
|
|
|
23
33
|
handler(event, matched);
|
|
24
34
|
}
|
|
25
35
|
};
|
|
26
|
-
|
|
36
|
+
const capture = NON_BUBBLING.has(type);
|
|
37
|
+
rootEl.addEventListener(type, listener, capture);
|
|
27
38
|
return () => {
|
|
28
|
-
rootEl.removeEventListener(type, listener);
|
|
39
|
+
rootEl.removeEventListener(type, listener, capture);
|
|
29
40
|
};
|
|
30
41
|
}
|
|
31
42
|
function delegateCapture(rootEl, type, selector, handler) {
|
|
@@ -44,25 +55,109 @@ function delegateCapture(rootEl, type, selector, handler) {
|
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// src/each.ts
|
|
47
|
-
var
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
51
65
|
}
|
|
52
66
|
function each(items, render, key) {
|
|
53
|
-
|
|
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
|
+
}
|
|
54
137
|
const segItems = new Array(items.length);
|
|
138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
55
139
|
for (let i = 0; i < items.length; i++) {
|
|
56
140
|
const item = items[i];
|
|
141
|
+
if (typeof item !== "object" || item === null) {
|
|
142
|
+
throw new Error(
|
|
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 })).`
|
|
144
|
+
);
|
|
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);
|
|
57
152
|
const k = key ? key(item, i) : void 0;
|
|
58
|
-
const cached = ROW_CACHE.get(item);
|
|
59
153
|
let html;
|
|
154
|
+
const cached = cache !== null ? cache.get(item) : void 0;
|
|
60
155
|
if (cached !== void 0 && cached.key === k) {
|
|
61
156
|
html = cached.html;
|
|
62
157
|
} else {
|
|
63
158
|
const out = render(item, i);
|
|
64
159
|
html = isSafeHtml(out) ? out.toString() : out;
|
|
65
|
-
|
|
160
|
+
if (cache !== null) cache.set(item, { key: k, html });
|
|
66
161
|
}
|
|
67
162
|
segItems[i] = { ref: item, cacheKey: k, html };
|
|
68
163
|
}
|
|
@@ -84,16 +179,23 @@ function getNodeKey(node) {
|
|
|
84
179
|
}
|
|
85
180
|
return void 0;
|
|
86
181
|
}
|
|
87
|
-
function diff(liveRoot, templateRoot,
|
|
88
|
-
diffChildren(liveRoot, templateRoot,
|
|
182
|
+
function diff(liveRoot, templateRoot, ownedItems) {
|
|
183
|
+
diffChildren(liveRoot, templateRoot, ownedItems);
|
|
89
184
|
}
|
|
90
|
-
function
|
|
185
|
+
function skipOwned(node, ownedItems) {
|
|
186
|
+
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
|
|
187
|
+
node = node.nextSibling;
|
|
188
|
+
}
|
|
189
|
+
return node;
|
|
190
|
+
}
|
|
191
|
+
function diffChildren(fromParent, toParent, ownedItems) {
|
|
91
192
|
const keyed = /* @__PURE__ */ new Map();
|
|
92
193
|
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
|
|
194
|
+
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
|
|
93
195
|
const k = getNodeKey(c);
|
|
94
196
|
if (k !== void 0) keyed.set(k, c);
|
|
95
197
|
}
|
|
96
|
-
let fromChild = fromParent.firstChild;
|
|
198
|
+
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
|
|
97
199
|
let toChild = toParent.firstChild;
|
|
98
200
|
while (toChild !== null) {
|
|
99
201
|
const toNext = toChild.nextSibling;
|
|
@@ -105,15 +207,15 @@ function diffChildren(fromParent, toParent, listParents) {
|
|
|
105
207
|
if (matched !== fromChild) {
|
|
106
208
|
fromParent.insertBefore(matched, fromChild);
|
|
107
209
|
} else {
|
|
108
|
-
fromChild = fromChild.nextSibling;
|
|
210
|
+
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
109
211
|
}
|
|
110
212
|
}
|
|
111
213
|
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0)) {
|
|
112
214
|
matched = fromChild;
|
|
113
|
-
fromChild = fromChild.nextSibling;
|
|
215
|
+
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
114
216
|
}
|
|
115
217
|
if (matched !== null) {
|
|
116
|
-
morphNode(matched, toChild,
|
|
218
|
+
morphNode(matched, toChild, ownedItems);
|
|
117
219
|
} else {
|
|
118
220
|
const cloned = toChild.cloneNode(true);
|
|
119
221
|
fromParent.insertBefore(cloned, fromChild);
|
|
@@ -122,13 +224,15 @@ function diffChildren(fromParent, toParent, listParents) {
|
|
|
122
224
|
}
|
|
123
225
|
while (fromChild !== null) {
|
|
124
226
|
const next = fromChild.nextSibling;
|
|
125
|
-
|
|
227
|
+
if (fromChild.nodeType !== ELEMENT_NODE || !ownedItems.has(fromChild)) {
|
|
228
|
+
fromParent.removeChild(fromChild);
|
|
229
|
+
}
|
|
126
230
|
fromChild = next;
|
|
127
231
|
}
|
|
128
232
|
}
|
|
129
|
-
function morphNode(fromNode, toNode,
|
|
233
|
+
function morphNode(fromNode, toNode, ownedItems) {
|
|
130
234
|
if (fromNode.nodeType === ELEMENT_NODE) {
|
|
131
|
-
morphElement(fromNode, toNode,
|
|
235
|
+
morphElement(fromNode, toNode, ownedItems);
|
|
132
236
|
return;
|
|
133
237
|
}
|
|
134
238
|
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
|
|
@@ -137,7 +241,7 @@ function morphNode(fromNode, toNode, listParents) {
|
|
|
137
241
|
if (fromText.data !== toText.data) fromText.data = toText.data;
|
|
138
242
|
}
|
|
139
243
|
}
|
|
140
|
-
function morphElement(fromEl, toEl,
|
|
244
|
+
function morphElement(fromEl, toEl, ownedItems) {
|
|
141
245
|
if (fromEl.tagName !== toEl.tagName) {
|
|
142
246
|
const replacement = toEl.cloneNode(true);
|
|
143
247
|
fromEl.parentNode?.replaceChild(replacement, fromEl);
|
|
@@ -151,8 +255,10 @@ function morphElement(fromEl, toEl, listParents) {
|
|
|
151
255
|
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
|
|
152
256
|
}
|
|
153
257
|
morphAttributes(fromEl, toEl);
|
|
154
|
-
|
|
155
|
-
|
|
258
|
+
diffChildren(fromEl, toEl, ownedItems);
|
|
259
|
+
}
|
|
260
|
+
function isUserAgentOwnedAttr(tagName, name) {
|
|
261
|
+
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
|
|
156
262
|
}
|
|
157
263
|
function morphAttributes(fromEl, toEl) {
|
|
158
264
|
const toAttrs = toEl.attributes;
|
|
@@ -170,13 +276,14 @@ function morphAttributes(fromEl, toEl) {
|
|
|
170
276
|
}
|
|
171
277
|
}
|
|
172
278
|
const fromAttrs = fromEl.attributes;
|
|
279
|
+
const fromTag = fromEl.tagName;
|
|
173
280
|
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
|
174
281
|
const attr = fromAttrs[i];
|
|
175
282
|
const ns = attr.namespaceURI;
|
|
176
283
|
const name = attr.localName;
|
|
177
284
|
if (ns !== null) {
|
|
178
285
|
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
|
|
179
|
-
} else if (!toEl.hasAttribute(name)) {
|
|
286
|
+
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
|
|
180
287
|
fromEl.removeAttribute(name);
|
|
181
288
|
}
|
|
182
289
|
}
|
|
@@ -201,13 +308,249 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
201
308
|
}
|
|
202
309
|
}
|
|
203
310
|
|
|
204
|
-
// src/list-
|
|
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
|
+
|
|
319
|
+
// src/list-reconcile-focus.ts
|
|
320
|
+
function captureFocus(liveParent) {
|
|
321
|
+
const active = document.activeElement;
|
|
322
|
+
if (active === null || active === document.body) return null;
|
|
323
|
+
if (!liveParent.contains(active)) return null;
|
|
324
|
+
const el = active;
|
|
325
|
+
let selStart = null;
|
|
326
|
+
let selEnd = null;
|
|
327
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
328
|
+
try {
|
|
329
|
+
selStart = el.selectionStart;
|
|
330
|
+
selEnd = el.selectionEnd;
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { el, selStart, selEnd };
|
|
335
|
+
}
|
|
336
|
+
function restoreFocus(snap) {
|
|
337
|
+
if (document.activeElement === snap.el) return;
|
|
338
|
+
if (!snap.el.isConnected) return;
|
|
339
|
+
snap.el.focus();
|
|
340
|
+
if (snap.selStart !== null && snap.selEnd !== null) {
|
|
341
|
+
try {
|
|
342
|
+
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
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
|
+
}
|
|
205
550
|
function classifyItems(oldItems, listSeg) {
|
|
206
551
|
const oldByRef = /* @__PURE__ */ new Map();
|
|
207
|
-
const oldIndex = /* @__PURE__ */ new Map();
|
|
208
552
|
for (let i = 0; i < oldItems.length; i++) {
|
|
209
|
-
oldByRef.set(oldItems[i].ref, oldItems[i]);
|
|
210
|
-
oldIndex.set(oldItems[i].ref, i);
|
|
553
|
+
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
|
|
211
554
|
}
|
|
212
555
|
const newRecord = new Array(listSeg.items.length);
|
|
213
556
|
const prevIdx = new Array(listSeg.items.length);
|
|
@@ -219,12 +562,12 @@ function classifyItems(oldItems, listSeg) {
|
|
|
219
562
|
const oi = oldByRef.get(ni.ref);
|
|
220
563
|
if (oi !== void 0) {
|
|
221
564
|
oldByRef.delete(ni.ref);
|
|
222
|
-
if (oi.html === ni.html) {
|
|
223
|
-
newRecord[i] = oi;
|
|
224
|
-
prevIdx[i] =
|
|
565
|
+
if (oi[0].html === ni.html) {
|
|
566
|
+
newRecord[i] = oi[0];
|
|
567
|
+
prevIdx[i] = oi[1];
|
|
225
568
|
continue;
|
|
226
569
|
}
|
|
227
|
-
replacedNodes.push(oi.node);
|
|
570
|
+
replacedNodes.push(oi[0].node);
|
|
228
571
|
}
|
|
229
572
|
newRecord[i] = {
|
|
230
573
|
ref: ni.ref,
|
|
@@ -236,32 +579,37 @@ function classifyItems(oldItems, listSeg) {
|
|
|
236
579
|
freshIndices.push(i);
|
|
237
580
|
freshHtmls.push(ni.html);
|
|
238
581
|
}
|
|
239
|
-
for (const [, orphan] of oldByRef) replacedNodes.push(orphan.node);
|
|
582
|
+
for (const [, orphan] of oldByRef) replacedNodes.push(orphan[0].node);
|
|
240
583
|
return { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls };
|
|
241
584
|
}
|
|
242
585
|
function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
243
586
|
if (freshHtmls.length === 0) return;
|
|
244
|
-
const tpl =
|
|
245
|
-
|
|
587
|
+
const { tpl, count } = parseRowTemplate(freshHtmls.join(""));
|
|
588
|
+
if (count !== freshHtmls.length) {
|
|
589
|
+
throw findOffendingRow(newRecord, freshIndices, freshHtmls);
|
|
590
|
+
}
|
|
246
591
|
let node = tpl.content.firstElementChild;
|
|
247
592
|
for (const idx of freshIndices) {
|
|
248
|
-
if (node === null) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`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)}`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
593
|
const next = node.nextElementSibling;
|
|
254
594
|
newRecord[idx].node = node;
|
|
255
595
|
node = next;
|
|
256
596
|
}
|
|
257
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
|
+
}
|
|
258
606
|
function removeOldNodes(liveParent, replacedNodes) {
|
|
259
607
|
for (const node of replacedNodes) {
|
|
260
608
|
if (node.parentElement === liveParent) liveParent.removeChild(node);
|
|
261
609
|
}
|
|
262
610
|
}
|
|
263
|
-
function applyMoves(liveParent, newRecord, prevIdx, stable) {
|
|
264
|
-
let nextSibling =
|
|
611
|
+
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
|
|
612
|
+
let nextSibling = tailAnchor;
|
|
265
613
|
for (let i = newRecord.length - 1; i >= 0; i--) {
|
|
266
614
|
const node = newRecord[i].node;
|
|
267
615
|
if (prevIdx[i] === -1 || !stable.has(i)) {
|
|
@@ -299,71 +647,138 @@ function lis(arr) {
|
|
|
299
647
|
}
|
|
300
648
|
return out;
|
|
301
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
|
|
302
658
|
function reconcileList(binding, listSeg) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
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);
|
|
309
664
|
}
|
|
310
665
|
|
|
311
666
|
// src/mount.ts
|
|
312
667
|
var LIST_MARKER_PREFIX = "kf-list:";
|
|
313
668
|
function mount(rootEl, render) {
|
|
669
|
+
if (rootEl == null) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
|
|
672
|
+
);
|
|
673
|
+
}
|
|
314
674
|
const bindings = /* @__PURE__ */ new Map();
|
|
315
|
-
const
|
|
675
|
+
const renderCtx = {
|
|
676
|
+
counter: 0,
|
|
677
|
+
caches: /* @__PURE__ */ new Map(),
|
|
678
|
+
bindingCounts: /* @__PURE__ */ new Map()
|
|
679
|
+
};
|
|
316
680
|
let isFirst = true;
|
|
681
|
+
let prevStaticHtml = "";
|
|
317
682
|
return effect(() => {
|
|
318
|
-
counter
|
|
319
|
-
|
|
683
|
+
renderCtx.counter = 0;
|
|
684
|
+
_setRenderContext(renderCtx);
|
|
320
685
|
let result;
|
|
321
686
|
try {
|
|
322
687
|
result = render();
|
|
323
688
|
} finally {
|
|
324
|
-
|
|
689
|
+
_setRenderContext(null);
|
|
325
690
|
}
|
|
326
|
-
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) };
|
|
327
692
|
if (isFirst) {
|
|
328
|
-
rootEl
|
|
329
|
-
|
|
693
|
+
runFirstRender(rootEl, segment, bindings);
|
|
694
|
+
prevStaticHtml = flattenWithoutListItems(segment);
|
|
330
695
|
isFirst = false;
|
|
331
696
|
} else {
|
|
332
|
-
|
|
333
|
-
template.innerHTML = flattenWithoutListItems(segment);
|
|
334
|
-
const listParents = /* @__PURE__ */ new Set();
|
|
335
|
-
for (const b of bindings.values()) listParents.add(b.liveParent);
|
|
336
|
-
diff(rootEl, template, listParents);
|
|
337
|
-
bindListsFromMarkers(rootEl, segment, bindings);
|
|
697
|
+
prevStaticHtml = runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml);
|
|
338
698
|
}
|
|
339
699
|
for (const listSeg of collectLists(segment).values()) {
|
|
340
700
|
const binding = bindings.get(listSeg.id);
|
|
341
701
|
reconcileList(binding, listSeg);
|
|
702
|
+
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
342
703
|
}
|
|
343
704
|
});
|
|
344
705
|
}
|
|
345
|
-
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) {
|
|
346
728
|
const lists = collectLists(segment);
|
|
347
729
|
const found = [];
|
|
348
730
|
collectComments(rootEl, found);
|
|
349
731
|
for (const marker of found) {
|
|
350
732
|
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
|
|
351
733
|
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
|
|
734
|
+
if (bindings.has(id)) continue;
|
|
352
735
|
const listSeg = lists.get(id);
|
|
353
736
|
const liveParent = marker.parentElement;
|
|
354
737
|
const items = [];
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
items.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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);
|
|
367
782
|
}
|
|
368
783
|
}
|
|
369
784
|
function collectComments(node, out) {
|