kerfjs 2.0.0 → 3.0.0-beta.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 +111 -0
- package/README.md +8 -0
- package/ai/cursorrules +11 -1
- package/ai/manifest.json +5 -5
- package/ai/skill.md +11 -1
- package/dist/array-signal.d.ts +7 -1
- package/dist/array-signal.js +11 -2
- package/dist/array-signal.js.map +1 -1
- package/dist/bindings-CYwoJpQb.d.ts +60 -0
- package/dist/chunk-3APBEVHF.js +20 -0
- package/dist/chunk-3APBEVHF.js.map +1 -0
- package/dist/chunk-GY4XV2UV.js +73 -0
- package/dist/chunk-GY4XV2UV.js.map +1 -0
- package/dist/{chunk-RYZHZBHE.js → chunk-JVVU2RQO.js} +15 -79
- package/dist/chunk-JVVU2RQO.js.map +1 -0
- package/dist/chunk-QIP723L4.js +15 -0
- package/dist/chunk-QIP723L4.js.map +1 -0
- package/dist/chunk-SAYPJ6XR.js +43 -0
- package/dist/chunk-SAYPJ6XR.js.map +1 -0
- package/dist/chunk-VVDJLWMP.js +14 -0
- package/dist/chunk-VVDJLWMP.js.map +1 -0
- package/dist/chunk-YHH7OUFA.js +58 -0
- package/dist/chunk-YHH7OUFA.js.map +1 -0
- package/dist/dev.d.ts +339 -0
- package/dist/dev.js +607 -0
- package/dist/dev.js.map +1 -0
- package/dist/html.d.ts +1 -0
- package/dist/html.js +4 -2
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +49 -11
- package/dist/index.js +358 -340
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +16 -60
- package/dist/jsx-runtime.js +4 -2
- package/dist/testing.js +3 -2
- package/llms.txt +2 -2
- package/package.json +22 -11
- package/dist/chunk-KFUDM3VP.js +0 -131
- package/dist/chunk-KFUDM3VP.js.map +0 -1
- package/dist/chunk-NU7YHYEV.js +0 -90
- package/dist/chunk-NU7YHYEV.js.map +0 -1
- package/dist/chunk-RYZHZBHE.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { itemVersion } from './chunk-QIP723L4.js';
|
|
2
|
+
import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js';
|
|
3
|
+
import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-JVVU2RQO.js';
|
|
4
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-JVVU2RQO.js';
|
|
5
|
+
import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js';
|
|
6
|
+
export { defineStore, resetAllStores } from './chunk-SAYPJ6XR.js';
|
|
7
|
+
import { effect } from './chunk-3APBEVHF.js';
|
|
8
|
+
export { batch, computed, effect, signal } from './chunk-3APBEVHF.js';
|
|
9
|
+
import { devHooks } from './chunk-VVDJLWMP.js';
|
|
6
10
|
|
|
7
11
|
// src/attrSelector.ts
|
|
8
12
|
function cssEscapeIdent(value) {
|
|
@@ -106,7 +110,7 @@ function makeListener(rootEl, selector, handler, match) {
|
|
|
106
110
|
}
|
|
107
111
|
function delegate(rootEl, type, selector, handler, options) {
|
|
108
112
|
assertValidSelector(selector, "delegate");
|
|
109
|
-
|
|
113
|
+
devHooks.delegateInEffect?.("delegate");
|
|
110
114
|
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
|
|
111
115
|
const capture = NON_BUBBLING.has(type);
|
|
112
116
|
rootEl.addEventListener(type, listener, capture);
|
|
@@ -116,7 +120,7 @@ function delegate(rootEl, type, selector, handler, options) {
|
|
|
116
120
|
}
|
|
117
121
|
function delegateCapture(rootEl, type, selector, handler, options) {
|
|
118
122
|
assertValidSelector(selector, "delegateCapture");
|
|
119
|
-
|
|
123
|
+
devHooks.delegateInEffect?.("delegateCapture");
|
|
120
124
|
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
|
|
121
125
|
rootEl.addEventListener(type, listener, true);
|
|
122
126
|
return () => {
|
|
@@ -124,52 +128,6 @@ function delegateCapture(rootEl, type, selector, handler, options) {
|
|
|
124
128
|
};
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
// src/dev-each-warn.ts
|
|
128
|
-
var warnedIds = /* @__PURE__ */ new Set();
|
|
129
|
-
function isOptedIn() {
|
|
130
|
-
if (!isDevMode()) return false;
|
|
131
|
-
const proc = globalThis.process;
|
|
132
|
-
return proc?.env?.KERF_DEV_WARN_EACH_IN_MORPH_SKIP === "1";
|
|
133
|
-
}
|
|
134
|
-
function hasMorphSkipAncestor(el, root) {
|
|
135
|
-
let ancestor = el.parentElement;
|
|
136
|
-
while (ancestor !== null && ancestor !== root) {
|
|
137
|
-
if (ancestor.dataset.morphSkip !== void 0) return true;
|
|
138
|
-
ancestor = ancestor.parentElement;
|
|
139
|
-
}
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
function maybeWarnEachInMorphSkip(id, liveParent, rootEl) {
|
|
143
|
-
if (!isOptedIn()) return;
|
|
144
|
-
if (warnedIds.has(id)) return;
|
|
145
|
-
if (!hasMorphSkipAncestor(liveParent, rootEl)) return;
|
|
146
|
-
warnedIds.add(id);
|
|
147
|
-
console.warn(
|
|
148
|
-
`kerf: each() list '${id}' is inside a data-morph-skip subtree. The keyed reconciler still updates the list rows, but any static signal-reactive JSX inside the same skipped ancestor (e.g. <p>{count.value}</p>) is frozen \u2014 the morph never visits it. Remove data-morph-skip from any element that contains reactive JSX content and reserve it for truly library-owned hosts. Set KERF_DEV_WARN_EACH_IN_MORPH_SKIP=0 (or unset it) to silence this warning.`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
var warnedDupIds = /* @__PURE__ */ new Set();
|
|
152
|
-
function isOptedInDupKeys() {
|
|
153
|
-
if (!isDevMode()) return false;
|
|
154
|
-
const proc = globalThis.process;
|
|
155
|
-
return proc?.env?.KERF_DEV_WARN_DUPLICATE_EACH_KEYS === "1";
|
|
156
|
-
}
|
|
157
|
-
function maybeWarnDuplicateCacheKeys(id, segItems) {
|
|
158
|
-
if (!isOptedInDupKeys()) return;
|
|
159
|
-
if (warnedDupIds.has(id)) return;
|
|
160
|
-
const seen = /* @__PURE__ */ new Set();
|
|
161
|
-
for (const si of segItems) {
|
|
162
|
-
if (seen.has(si.cacheKey)) {
|
|
163
|
-
warnedDupIds.add(id);
|
|
164
|
-
console.warn(
|
|
165
|
-
`kerf: each() list '${id}' has duplicate cacheKey values (duplicate: ${String(si.cacheKey)}). The cacheKey function should return a unique value per row so kerf can tell apart items for memoization \u2014 duplicate values cause some rows to return stale cached HTML when external state that affects their render changes. Set KERF_DEV_WARN_DUPLICATE_EACH_KEYS=0 (or unset it) to silence this warning.`
|
|
166
|
-
);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
seen.add(si.cacheKey);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
131
|
// src/list-render-state.ts
|
|
174
132
|
function deriveListRenderState(bindingCount) {
|
|
175
133
|
if (bindingCount === void 0) return "unbound";
|
|
@@ -198,60 +156,138 @@ function isArraySignal(value) {
|
|
|
198
156
|
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
|
|
199
157
|
}
|
|
200
158
|
var context = null;
|
|
159
|
+
var renderingRow = false;
|
|
160
|
+
function inRowScope(fn) {
|
|
161
|
+
const prev = renderingRow;
|
|
162
|
+
renderingRow = true;
|
|
163
|
+
try {
|
|
164
|
+
return fn();
|
|
165
|
+
} finally {
|
|
166
|
+
renderingRow = prev;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
201
169
|
function _setRenderContext(c) {
|
|
202
170
|
context = c;
|
|
203
171
|
}
|
|
204
|
-
function
|
|
172
|
+
function _resetCallOrderListState(ctx) {
|
|
173
|
+
const isCallOrderId = (id) => !id.startsWith("k:");
|
|
174
|
+
for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) {
|
|
175
|
+
for (const id of Array.from(map.keys())) {
|
|
176
|
+
if (isCallOrderId(id)) map.delete(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function isEachOptions(v) {
|
|
181
|
+
return typeof v === "object" && v !== null;
|
|
182
|
+
}
|
|
183
|
+
var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/;
|
|
184
|
+
function assertValidKey(key) {
|
|
185
|
+
if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function claimKey(ctx, key) {
|
|
192
|
+
assertValidKey(key);
|
|
193
|
+
if (renderingRow) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (ctx.keysThisRender.has(key)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
ctx.keysThisRender.add(key);
|
|
204
|
+
return `k:${key}`;
|
|
205
|
+
}
|
|
206
|
+
function each(items, render, cacheKeyOrOptions) {
|
|
207
|
+
const useOptions = isEachOptions(cacheKeyOrOptions);
|
|
208
|
+
const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions;
|
|
209
|
+
const listKey = useOptions ? cacheKeyOrOptions.key : void 0;
|
|
205
210
|
if (isArraySignal(items) && context !== null) {
|
|
206
|
-
return eachGranular(items, render, cacheKey);
|
|
211
|
+
return eachGranular(items, render, cacheKey, listKey);
|
|
207
212
|
}
|
|
208
213
|
const snapshotItems = isArraySignal(items) ? items.value : items;
|
|
209
|
-
return eachSnapshot(snapshotItems, render, cacheKey);
|
|
214
|
+
return eachSnapshot(snapshotItems, render, cacheKey, listKey);
|
|
210
215
|
}
|
|
211
|
-
function eachSnapshot(items, render, cacheKey) {
|
|
216
|
+
function eachSnapshot(items, render, cacheKey, listKey) {
|
|
212
217
|
let id;
|
|
213
218
|
if (context !== null) {
|
|
214
|
-
id = String(context.counter++);
|
|
219
|
+
id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++);
|
|
215
220
|
} else {
|
|
216
221
|
id = "orphan";
|
|
217
222
|
}
|
|
218
223
|
return eachSnapshotById(items, render, cacheKey, id);
|
|
219
224
|
}
|
|
220
|
-
function
|
|
225
|
+
function assertObjectItem(item, index) {
|
|
226
|
+
if (typeof item !== "object" || item === null) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function eachGranular(sig, render, cacheKey, listKey) {
|
|
221
233
|
const ctx = context;
|
|
222
|
-
const id = String(ctx.counter++);
|
|
234
|
+
const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++);
|
|
223
235
|
const previousBindingCount = ctx.bindingCounts.get(id);
|
|
224
236
|
const patches = sig._consumePatches();
|
|
225
237
|
const snapshot = sig.value;
|
|
226
|
-
const
|
|
238
|
+
const previousSource = ctx.bindingSources.get(id);
|
|
239
|
+
const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig;
|
|
240
|
+
if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id);
|
|
241
|
+
const decision = sourceReused ? { path: "snapshot" } : decideListPath(
|
|
227
242
|
deriveListRenderState(previousBindingCount),
|
|
228
243
|
patches,
|
|
229
244
|
snapshot.length,
|
|
230
245
|
previousBindingCount
|
|
231
246
|
);
|
|
232
247
|
if (decision.path === "snapshot") {
|
|
233
|
-
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
248
|
+
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
|
|
249
|
+
}
|
|
250
|
+
let staleIndexShift = false;
|
|
251
|
+
if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
|
|
252
|
+
const rendered = [];
|
|
253
|
+
for (let i = 0; i < previousBindingCount; i++) rendered.push(i);
|
|
254
|
+
for (const p of patches) {
|
|
255
|
+
if (p.type === "insert") rendered.splice(p.index, 0, p.index);
|
|
256
|
+
else if (p.type === "remove") rendered.splice(p.index, 1);
|
|
257
|
+
else if (p.type === "move") {
|
|
258
|
+
const [moved] = rendered.splice(p.from, 1);
|
|
259
|
+
rendered.splice(p.to, 0, moved);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
263
|
+
if (rendered[i] !== i) {
|
|
264
|
+
staleIndexShift = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
234
268
|
}
|
|
235
269
|
if (cacheKey !== void 0) {
|
|
236
|
-
const
|
|
270
|
+
const cache2 = ctx.caches.get(id);
|
|
237
271
|
for (let i = 0; i < snapshot.length; i++) {
|
|
238
272
|
const item = snapshot[i];
|
|
239
273
|
const k = cacheKey(item, i);
|
|
240
|
-
const cached =
|
|
274
|
+
const cached = cache2.get(item);
|
|
241
275
|
if (cached !== void 0 && cached.cacheKey !== k) {
|
|
242
|
-
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
276
|
+
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
|
|
243
277
|
}
|
|
244
278
|
}
|
|
245
279
|
}
|
|
246
|
-
const renderRow = (item, index) => captureRowBindings(() => {
|
|
280
|
+
const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => {
|
|
247
281
|
const out = render(item, index);
|
|
248
282
|
return isSafeHtml(out) ? out.toString() : out;
|
|
249
|
-
});
|
|
283
|
+
}));
|
|
250
284
|
const internalPatches = new Array(patches.length);
|
|
285
|
+
const cache = ctx.caches.get(id);
|
|
251
286
|
try {
|
|
252
287
|
for (let i = 0; i < patches.length; i++) {
|
|
253
288
|
const p = patches[i];
|
|
254
289
|
if (p.type === "insert" || p.type === "update") {
|
|
290
|
+
assertObjectItem(p.item, p.index);
|
|
255
291
|
const { html, bindings } = renderRow(p.item, p.index);
|
|
256
292
|
internalPatches[i] = {
|
|
257
293
|
type: p.type,
|
|
@@ -260,17 +296,25 @@ function eachGranular(sig, render, cacheKey) {
|
|
|
260
296
|
html,
|
|
261
297
|
bindings
|
|
262
298
|
};
|
|
299
|
+
cache?.set(p.item, {
|
|
300
|
+
cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0,
|
|
301
|
+
html,
|
|
302
|
+
bindings,
|
|
303
|
+
version: itemVersion(p.item),
|
|
304
|
+
index: p.index
|
|
305
|
+
});
|
|
263
306
|
} else {
|
|
264
307
|
internalPatches[i] = p;
|
|
265
308
|
}
|
|
266
309
|
}
|
|
267
310
|
} catch {
|
|
268
311
|
ctx.bindingCounts.delete(id);
|
|
269
|
-
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
312
|
+
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
|
|
270
313
|
}
|
|
271
|
-
|
|
314
|
+
if (staleIndexShift) devHooks.staleIndex?.(id);
|
|
315
|
+
return granularListSafeHtml(id, [], internalPatches, sig);
|
|
272
316
|
}
|
|
273
|
-
function eachSnapshotById(items, render, cacheKey, id) {
|
|
317
|
+
function eachSnapshotById(items, render, cacheKey, id, source) {
|
|
274
318
|
let cache = null;
|
|
275
319
|
if (context !== null) {
|
|
276
320
|
let c = context.caches.get(id);
|
|
@@ -284,11 +328,7 @@ function eachSnapshotById(items, render, cacheKey, id) {
|
|
|
284
328
|
const seen = /* @__PURE__ */ new Set();
|
|
285
329
|
for (let i = 0; i < items.length; i++) {
|
|
286
330
|
const item = items[i];
|
|
287
|
-
|
|
288
|
-
throw new Error(
|
|
289
|
-
`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 })).`
|
|
290
|
-
);
|
|
291
|
-
}
|
|
331
|
+
assertObjectItem(item, i);
|
|
292
332
|
if (seen.has(item)) {
|
|
293
333
|
throw new Error(
|
|
294
334
|
`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()).`
|
|
@@ -296,27 +336,60 @@ function eachSnapshotById(items, render, cacheKey, id) {
|
|
|
296
336
|
}
|
|
297
337
|
seen.add(item);
|
|
298
338
|
const k = cacheKey ? cacheKey(item, i) : void 0;
|
|
339
|
+
const version = itemVersion(item);
|
|
299
340
|
let html;
|
|
300
341
|
let bindings;
|
|
301
342
|
const cached = cache !== null ? cache.get(item) : void 0;
|
|
302
|
-
if (cached !== void 0 && cached.cacheKey === k) {
|
|
343
|
+
if (cached !== void 0 && cached.cacheKey === k && cached.version === version) {
|
|
303
344
|
html = cached.html;
|
|
304
345
|
bindings = cached.bindings;
|
|
346
|
+
if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
|
|
347
|
+
devHooks.staleIndex?.(id);
|
|
348
|
+
}
|
|
305
349
|
} else {
|
|
306
|
-
const captured = captureRowBindings(() => {
|
|
350
|
+
const captured = captureRowBindings(() => inRowScope(() => {
|
|
307
351
|
const out = render(item, i);
|
|
308
352
|
return isSafeHtml(out) ? out.toString() : out;
|
|
309
|
-
});
|
|
353
|
+
}));
|
|
310
354
|
html = captured.html;
|
|
311
355
|
bindings = captured.bindings;
|
|
312
|
-
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings });
|
|
356
|
+
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i });
|
|
313
357
|
}
|
|
314
358
|
segItems[i] = { ref: item, cacheKey: k, html, bindings };
|
|
315
359
|
}
|
|
316
360
|
if (cacheKey !== void 0) {
|
|
317
|
-
|
|
361
|
+
devHooks.duplicateCacheKeys?.(id, segItems);
|
|
362
|
+
}
|
|
363
|
+
return listSafeHtml(id, segItems, source);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/list-reconcile-focus.ts
|
|
367
|
+
function captureFocus(liveParent) {
|
|
368
|
+
const active = document.activeElement;
|
|
369
|
+
if (active === null || active === document.body) return null;
|
|
370
|
+
if (!liveParent.contains(active)) return null;
|
|
371
|
+
const el = active;
|
|
372
|
+
let selStart = null;
|
|
373
|
+
let selEnd = null;
|
|
374
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
375
|
+
try {
|
|
376
|
+
selStart = el.selectionStart;
|
|
377
|
+
selEnd = el.selectionEnd;
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { el, selStart, selEnd };
|
|
382
|
+
}
|
|
383
|
+
function restoreFocus(snap) {
|
|
384
|
+
if (document.activeElement === snap.el) return;
|
|
385
|
+
if (!snap.el.isConnected) return;
|
|
386
|
+
snap.el.focus();
|
|
387
|
+
if (snap.selStart !== null && snap.selEnd !== null) {
|
|
388
|
+
try {
|
|
389
|
+
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
318
392
|
}
|
|
319
|
-
return listSafeHtml(id, segItems);
|
|
320
393
|
}
|
|
321
394
|
|
|
322
395
|
// src/morph.ts
|
|
@@ -342,7 +415,9 @@ function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
|
|
|
342
415
|
);
|
|
343
416
|
}
|
|
344
417
|
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
|
|
418
|
+
const focusSnap = captureFocus(liveRoot);
|
|
345
419
|
morphChildren(liveRoot, templateEl, ownedItems);
|
|
420
|
+
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
346
421
|
}
|
|
347
422
|
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
|
|
348
423
|
morphElement(fromEl, toEl, ownedItems);
|
|
@@ -355,12 +430,37 @@ function parseTemplate(liveRoot, template) {
|
|
|
355
430
|
el.innerHTML = String(template);
|
|
356
431
|
return el;
|
|
357
432
|
}
|
|
433
|
+
function protectionTag(node) {
|
|
434
|
+
const { dataset } = node;
|
|
435
|
+
return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : "");
|
|
436
|
+
}
|
|
437
|
+
var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX];
|
|
438
|
+
function isMarker(node) {
|
|
439
|
+
if (node.nodeType !== COMMENT_NODE) return false;
|
|
440
|
+
const { data } = node;
|
|
441
|
+
return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix));
|
|
442
|
+
}
|
|
443
|
+
function markersPairable(a, b) {
|
|
444
|
+
if (!isMarker(a) && !isMarker(b)) return true;
|
|
445
|
+
return a.data === b.data;
|
|
446
|
+
}
|
|
358
447
|
function skipOwned(node, ownedItems) {
|
|
359
448
|
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
|
|
360
449
|
node = node.nextSibling;
|
|
361
450
|
}
|
|
362
451
|
return node;
|
|
363
452
|
}
|
|
453
|
+
function isListMarker(node) {
|
|
454
|
+
return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX);
|
|
455
|
+
}
|
|
456
|
+
function afterListRegion(marker, ownedItems) {
|
|
457
|
+
let last = marker;
|
|
458
|
+
for (let r = marker.nextSibling; r !== null; r = r.nextSibling) {
|
|
459
|
+
if (isListMarker(r)) break;
|
|
460
|
+
if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r;
|
|
461
|
+
}
|
|
462
|
+
return last.nextSibling;
|
|
463
|
+
}
|
|
364
464
|
function morphChildren(fromParent, toParent, ownedItems) {
|
|
365
465
|
const keyed = /* @__PURE__ */ new Map();
|
|
366
466
|
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
|
|
@@ -383,9 +483,47 @@ function morphChildren(fromParent, toParent, ownedItems) {
|
|
|
383
483
|
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
384
484
|
}
|
|
385
485
|
}
|
|
386
|
-
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0)) {
|
|
486
|
+
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) {
|
|
387
487
|
matched = fromChild;
|
|
388
|
-
fromChild = skipOwned(
|
|
488
|
+
fromChild = skipOwned(
|
|
489
|
+
isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling,
|
|
490
|
+
ownedItems
|
|
491
|
+
);
|
|
492
|
+
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
|
|
493
|
+
const owned = boundTextNodeOf(matched);
|
|
494
|
+
if (owned !== null && fromChild === owned) {
|
|
495
|
+
fromChild = skipOwned(owned.nextSibling, ownedItems);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) {
|
|
500
|
+
const toTag = toChild.tagName;
|
|
501
|
+
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
|
|
502
|
+
if (scan.nodeType !== ELEMENT_NODE) continue;
|
|
503
|
+
const el = scan;
|
|
504
|
+
if (ownedItems.has(el)) continue;
|
|
505
|
+
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
|
|
506
|
+
if (protectionTag(el) !== protectionTag(toChild)) continue;
|
|
507
|
+
matched = el;
|
|
508
|
+
fromParent.insertBefore(el, fromChild);
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) {
|
|
513
|
+
const wantData = toChild.data;
|
|
514
|
+
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
|
|
515
|
+
if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue;
|
|
516
|
+
const regionEnd = afterListRegion(scan, ownedItems);
|
|
517
|
+
const run = [];
|
|
518
|
+
for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) {
|
|
519
|
+
run.push(r);
|
|
520
|
+
}
|
|
521
|
+
const focusSnap = captureFocus(fromParent);
|
|
522
|
+
for (const node of run) fromParent.insertBefore(node, fromChild);
|
|
523
|
+
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
524
|
+
matched = scan;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
389
527
|
}
|
|
390
528
|
if (matched !== null) {
|
|
391
529
|
morphNode(matched, toChild, ownedItems);
|
|
@@ -493,144 +631,12 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
493
631
|
}
|
|
494
632
|
}
|
|
495
633
|
|
|
496
|
-
// src/dev-binding-warn.ts
|
|
497
|
-
var warnedHoles = /* @__PURE__ */ new Set();
|
|
498
|
-
function isOptedIn2() {
|
|
499
|
-
if (!isDevMode()) return false;
|
|
500
|
-
const proc = globalThis.process;
|
|
501
|
-
return proc?.env?.KERF_DEV_WARN_STALE_BINDING === "1";
|
|
502
|
-
}
|
|
503
|
-
function describeHole(b) {
|
|
504
|
-
return b.kind === "attr" ? `attr '${b.attr}' (id '${b.id}')` : `text (id '${b.id}')`;
|
|
505
|
-
}
|
|
506
|
-
function maybeWarnStaleBinding(prevWired, current) {
|
|
507
|
-
if (!isOptedIn2()) return;
|
|
508
|
-
const n = Math.min(prevWired.length, current.length);
|
|
509
|
-
for (let i = 0; i < n; i++) {
|
|
510
|
-
const cur = current[i];
|
|
511
|
-
if (prevWired[i].signal === cur.signal) continue;
|
|
512
|
-
if (warnedHoles.has(cur.id)) continue;
|
|
513
|
-
warnedHoles.add(cur.id);
|
|
514
|
-
console.warn(
|
|
515
|
-
`kerf: fine-grained binding ${describeHole(cur)} switched to a different signal instance on a render whose static-surrounds HTML was byte-for-byte unchanged. On that fast path kerf keeps the original binding effect and does NOT re-bind, so this hole is now stale \u2014 it still tracks the FIRST signal instance and will not reflect the new one. Bind one computed that switches internally (e.g. class={computed(() => cond.value ? sigA.value : sigB.value)}) instead of switching which signal instance you bind (see docs/2-reactivity \xA72.9). Set KERF_DEV_WARN_STALE_BINDING=0 (or unset it) to silence this warning.`
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// src/dev-listener-warn.ts
|
|
521
|
-
var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
|
|
522
|
-
var patched = false;
|
|
523
|
-
var warned = false;
|
|
524
|
-
function isOptedIn3() {
|
|
525
|
-
if (!isDevMode()) return false;
|
|
526
|
-
const proc = globalThis.process;
|
|
527
|
-
return proc?.env?.KERF_DEV_WARN_REBUILT_LISTENERS === "1";
|
|
528
|
-
}
|
|
529
|
-
function findAddEventListenerProto() {
|
|
530
|
-
const probe = document.createElement("div");
|
|
531
|
-
let proto = Object.getPrototypeOf(probe);
|
|
532
|
-
while (!Object.prototype.hasOwnProperty.call(proto, "addEventListener")) {
|
|
533
|
-
proto = Object.getPrototypeOf(proto);
|
|
534
|
-
}
|
|
535
|
-
return proto;
|
|
536
|
-
}
|
|
537
|
-
function patchAddEventListenerOnce() {
|
|
538
|
-
if (patched) return;
|
|
539
|
-
patched = true;
|
|
540
|
-
const proto = findAddEventListenerProto();
|
|
541
|
-
const orig = proto.addEventListener;
|
|
542
|
-
proto.addEventListener = function(type, listener, options) {
|
|
543
|
-
if (this instanceof Element) {
|
|
544
|
-
this[LISTENER_MARKER] = true;
|
|
545
|
-
}
|
|
546
|
-
return orig.call(this, type, listener, options);
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function hasMarkedListener(el) {
|
|
550
|
-
if (el[LISTENER_MARKER] === true) return true;
|
|
551
|
-
const stack = [];
|
|
552
|
-
for (let i = 0; i < el.children.length; i++) stack.push(el.children[i]);
|
|
553
|
-
while (stack.length > 0) {
|
|
554
|
-
const cur = stack.pop();
|
|
555
|
-
if (cur[LISTENER_MARKER] === true) return true;
|
|
556
|
-
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
function emitWarning() {
|
|
561
|
-
if (warned) return;
|
|
562
|
-
warned = true;
|
|
563
|
-
console.warn(
|
|
564
|
-
"kerf: a node inside a mount()-managed tree was removed/rebuilt while carrying an imperative addEventListener listener. The listener is gone with the old node. Use `delegate(rootEl, 'click', '[data-action=\"...\"]', handler)` so the listener lives on a stable ancestor and survives re-renders, or wrap the host in `data-morph-skip` if the subtree is library-owned (Monaco, xterm, D3 charts). Set KERF_DEV_WARN_REBUILT_LISTENERS=0 (or unset it) to silence this warning."
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
function installListenerRebuildWarn(rootEl) {
|
|
568
|
-
if (!isOptedIn3()) return null;
|
|
569
|
-
patchAddEventListenerOnce();
|
|
570
|
-
const observer = new MutationObserver((mutations) => {
|
|
571
|
-
if (warned) return;
|
|
572
|
-
for (const m of mutations) {
|
|
573
|
-
for (let i = 0; i < m.removedNodes.length; i++) {
|
|
574
|
-
const removed = m.removedNodes[i];
|
|
575
|
-
if (!(removed instanceof Element)) continue;
|
|
576
|
-
if (hasMarkedListener(removed)) {
|
|
577
|
-
emitWarning();
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
observer.observe(rootEl, { childList: true, subtree: true });
|
|
584
|
-
return observer;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/dev-rerender-warn.ts
|
|
588
|
-
function isOptedIn4() {
|
|
589
|
-
const proc = globalThis.process;
|
|
590
|
-
if (proc?.env?.KERF_DEV_WARN_VALUE_ONLY_RERENDER !== "1") return false;
|
|
591
|
-
return isDevMode();
|
|
592
|
-
}
|
|
593
|
-
var ELEMENT_NODE2 = 1;
|
|
594
|
-
var TEXT_NODE2 = 3;
|
|
595
|
-
var COMMENT_NODE2 = 8;
|
|
596
|
-
function _isValueOnlyDiff(a, b) {
|
|
597
|
-
const an = a.childNodes;
|
|
598
|
-
const bn = b.childNodes;
|
|
599
|
-
if (an.length !== bn.length) return false;
|
|
600
|
-
for (let i = 0; i < an.length; i++) {
|
|
601
|
-
const x = an[i];
|
|
602
|
-
const y = bn[i];
|
|
603
|
-
if (x.nodeType !== y.nodeType) return false;
|
|
604
|
-
if (x.nodeType === ELEMENT_NODE2) {
|
|
605
|
-
if (x.tagName !== y.tagName) return false;
|
|
606
|
-
if (!_isValueOnlyDiff(x, y)) return false;
|
|
607
|
-
} else if (x.nodeType === COMMENT_NODE2) {
|
|
608
|
-
if (x.data !== y.data) return false;
|
|
609
|
-
} else if (x.nodeType !== TEXT_NODE2) {
|
|
610
|
-
return false;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return true;
|
|
614
|
-
}
|
|
615
|
-
function maybeWarnValueOnlyRerender(prevHtml, nextHtml, ctx) {
|
|
616
|
-
if (ctx.warned || !isOptedIn4()) return;
|
|
617
|
-
const a = document.createElement("template");
|
|
618
|
-
const b = document.createElement("template");
|
|
619
|
-
a.innerHTML = prevHtml;
|
|
620
|
-
b.innerHTML = nextHtml;
|
|
621
|
-
if (!_isValueOnlyDiff(a.content, b.content)) return;
|
|
622
|
-
ctx.warned = true;
|
|
623
|
-
console.warn(
|
|
624
|
-
"kerf: this re-render changed only text content and attribute values \u2014 no structural change \u2014 so every changed hole could be a fine-grained binding instead. Values bind, structure re-renders: pass the signal/computed itself ({count}, class={sig}) rather than reading .value in the hole, and each change updates just that node with no render re-run (a mount whose render reads no .value never re-renders at all). See docs/2-reactivity \xA72.9. Set KERF_DEV_WARN_VALUE_ONLY_RERENDER=0 (or unset it) to silence this warning."
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
634
|
// src/list-binding.ts
|
|
629
635
|
function endAnchor(binding) {
|
|
630
636
|
if (binding.items.length > 0) {
|
|
631
|
-
return binding.items[binding.items.length - 1].node.
|
|
637
|
+
return binding.items[binding.items.length - 1].node.nextSibling;
|
|
632
638
|
}
|
|
633
|
-
return binding.marker.
|
|
639
|
+
return binding.marker.nextSibling;
|
|
634
640
|
}
|
|
635
641
|
|
|
636
642
|
// src/list-reconcile-fast-paths.ts
|
|
@@ -641,8 +647,8 @@ var SQUOTE = 39;
|
|
|
641
647
|
var AMP = 38;
|
|
642
648
|
var EQ = 61;
|
|
643
649
|
var SLASH = 47;
|
|
644
|
-
var
|
|
645
|
-
var
|
|
650
|
+
var TEXT_NODE2 = 3;
|
|
651
|
+
var ELEMENT_NODE2 = 1;
|
|
646
652
|
function isWhitespace(cc) {
|
|
647
653
|
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
|
|
648
654
|
}
|
|
@@ -667,12 +673,15 @@ function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
|
|
|
667
673
|
for (const [name, rawValue] of newTag.attrs) {
|
|
668
674
|
const oldValue = oldTag.attrs.get(name);
|
|
669
675
|
if (oldValue === rawValue) continue;
|
|
670
|
-
|
|
676
|
+
const value = unescapeAttrValue(rawValue);
|
|
677
|
+
liveNode.setAttribute(name, value);
|
|
678
|
+
syncFormProp(liveNode, name, value, true);
|
|
671
679
|
}
|
|
672
680
|
for (const name of oldTag.attrs.keys()) {
|
|
673
681
|
if (newTag.attrs.has(name)) continue;
|
|
674
682
|
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
|
|
675
683
|
liveNode.removeAttribute(name);
|
|
684
|
+
syncFormProp(liveNode, name, "", false);
|
|
676
685
|
}
|
|
677
686
|
return true;
|
|
678
687
|
}
|
|
@@ -701,11 +710,16 @@ function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
|
|
|
701
710
|
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
|
|
702
711
|
const oldText = oldHtml.slice(textStart + 1, textEnd);
|
|
703
712
|
const newText = newHtml.slice(textStart + 1, newTextEnd);
|
|
713
|
+
if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false;
|
|
704
714
|
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
|
|
705
715
|
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
|
|
706
716
|
if (targetNode === null) return false;
|
|
707
717
|
if (targetNode.nodeValue !== oldText) return false;
|
|
708
718
|
targetNode.nodeValue = newText;
|
|
719
|
+
const host = targetNode.parentNode;
|
|
720
|
+
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
|
|
721
|
+
host.value = newText;
|
|
722
|
+
}
|
|
709
723
|
return true;
|
|
710
724
|
}
|
|
711
725
|
function containsDataMorphSkip(html) {
|
|
@@ -745,13 +759,13 @@ function nthTextNodeDescendant(root, n) {
|
|
|
745
759
|
function walk(node) {
|
|
746
760
|
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
747
761
|
if (result !== null) return;
|
|
748
|
-
if (c.nodeType ===
|
|
762
|
+
if (c.nodeType === TEXT_NODE2) {
|
|
749
763
|
if (count === n) {
|
|
750
764
|
result = c;
|
|
751
765
|
return;
|
|
752
766
|
}
|
|
753
767
|
count++;
|
|
754
|
-
} else if (c.nodeType ===
|
|
768
|
+
} else if (c.nodeType === ELEMENT_NODE2) {
|
|
755
769
|
walk(c);
|
|
756
770
|
}
|
|
757
771
|
}
|
|
@@ -811,76 +825,6 @@ function isUserAgentOwnedAttr2(tagNameUpper, name) {
|
|
|
811
825
|
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
|
|
812
826
|
}
|
|
813
827
|
|
|
814
|
-
// src/list-reconcile-focus.ts
|
|
815
|
-
function captureFocus(liveParent) {
|
|
816
|
-
const active = document.activeElement;
|
|
817
|
-
if (active === null || active === document.body) return null;
|
|
818
|
-
if (!liveParent.contains(active)) return null;
|
|
819
|
-
const el = active;
|
|
820
|
-
let selStart = null;
|
|
821
|
-
let selEnd = null;
|
|
822
|
-
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
823
|
-
try {
|
|
824
|
-
selStart = el.selectionStart;
|
|
825
|
-
selEnd = el.selectionEnd;
|
|
826
|
-
} catch {
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return { el, selStart, selEnd };
|
|
830
|
-
}
|
|
831
|
-
function restoreFocus(snap) {
|
|
832
|
-
if (document.activeElement === snap.el) return;
|
|
833
|
-
if (!snap.el.isConnected) return;
|
|
834
|
-
snap.el.focus();
|
|
835
|
-
if (snap.selStart !== null && snap.selEnd !== null) {
|
|
836
|
-
try {
|
|
837
|
-
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
|
|
838
|
-
} catch {
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// src/utils/rowContract.ts
|
|
844
|
-
var ROW_HTML_SNIPPET_MAX = 120;
|
|
845
|
-
function truncateRowHtml(html) {
|
|
846
|
-
return html.length > ROW_HTML_SNIPPET_MAX ? html.slice(0, ROW_HTML_SNIPPET_MAX) + "\u2026" : html;
|
|
847
|
-
}
|
|
848
|
-
function parseRowTemplate(html) {
|
|
849
|
-
const tpl = document.createElement("template");
|
|
850
|
-
tpl.innerHTML = html;
|
|
851
|
-
return { tpl, count: tpl.content.children.length };
|
|
852
|
-
}
|
|
853
|
-
function parseSingleRow(html, index) {
|
|
854
|
-
const { tpl, count } = parseRowTemplate(html);
|
|
855
|
-
if (count !== 1) throw rowContractError(index, html);
|
|
856
|
-
return tpl.content.firstElementChild;
|
|
857
|
-
}
|
|
858
|
-
function collectTemplateChildren(tpl, n) {
|
|
859
|
-
const nodes = new Array(n);
|
|
860
|
-
let child = tpl.content.firstElementChild;
|
|
861
|
-
for (let k = 0; k < n; k++) {
|
|
862
|
-
nodes[k] = child;
|
|
863
|
-
child = child.nextElementSibling;
|
|
864
|
-
}
|
|
865
|
-
return nodes;
|
|
866
|
-
}
|
|
867
|
-
function rowContractError(index, html) {
|
|
868
|
-
const { count } = parseRowTemplate(html);
|
|
869
|
-
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
870
|
-
return new Error(
|
|
871
|
-
`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))}`
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
function maybeWarnMissingRowKey(rowEl, rowHtml, binding) {
|
|
875
|
-
if (!isDevMode()) return;
|
|
876
|
-
if (binding.warnedMissingKey === true) return;
|
|
877
|
-
binding.warnedMissingKey = true;
|
|
878
|
-
if (rowEl.id !== "" || rowEl.hasAttribute("data-key")) return;
|
|
879
|
-
console.warn(
|
|
880
|
-
`kerf each(): the first row has no \`id\` or \`data-key\` attribute. Without one, rows match positionally \u2014 an insert/remove at the head shifts every row's identity, so focused inputs jump to the wrong row, mid-edit textareas swap content with their neighbor, and any per-row state silently follows the wrong item. Add \`data-key={item.id}\` (or set \`id\`) to the top-level element returned by the row render. Row HTML: ${JSON.stringify(truncateRowHtml(rowHtml))}`
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
828
|
// src/list-reconcile-granular.ts
|
|
885
829
|
function reconcileGranular(binding, patches) {
|
|
886
830
|
const { liveParent } = binding;
|
|
@@ -943,12 +887,12 @@ function reconcileGranular(binding, patches) {
|
|
|
943
887
|
}
|
|
944
888
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
945
889
|
if (items.length > 0) {
|
|
946
|
-
|
|
890
|
+
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
|
|
947
891
|
}
|
|
948
892
|
}
|
|
949
893
|
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
950
894
|
const { html } = patch;
|
|
951
|
-
const newNode = parseSingleRow(html, patch.index);
|
|
895
|
+
const newNode = parseSingleRow(html, patch.index, liveParent);
|
|
952
896
|
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
|
|
953
897
|
liveParent.insertBefore(newNode, anchor);
|
|
954
898
|
items.splice(patch.index, 0, {
|
|
@@ -975,7 +919,7 @@ function applySingleUpdate(liveParent, items, patch) {
|
|
|
975
919
|
items[patch.index] = reuseBound(patch, html, oldEntry);
|
|
976
920
|
return;
|
|
977
921
|
}
|
|
978
|
-
const newNode = parseSingleRow(html, patch.index);
|
|
922
|
+
const newNode = parseSingleRow(html, patch.index, liveParent);
|
|
979
923
|
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
|
|
980
924
|
}
|
|
981
925
|
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
|
|
@@ -1028,11 +972,11 @@ function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
|
1028
972
|
morphChanges.push({ patchIdx: k, html: p.html });
|
|
1029
973
|
}
|
|
1030
974
|
if (morphChanges.length === 0) return;
|
|
1031
|
-
const {
|
|
975
|
+
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
|
|
1032
976
|
if (count !== morphChanges.length) {
|
|
1033
|
-
throw findOffendingChange(patches, morphChanges);
|
|
977
|
+
throw findOffendingChange(patches, morphChanges, liveParent);
|
|
1034
978
|
}
|
|
1035
|
-
const newNodes = collectTemplateChildren(
|
|
979
|
+
const newNodes = collectTemplateChildren(content, morphChanges.length);
|
|
1036
980
|
for (let k = 0; k < morphChanges.length; k++) {
|
|
1037
981
|
const c = morphChanges[k];
|
|
1038
982
|
const p = patches[c.patchIdx];
|
|
@@ -1045,13 +989,13 @@ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
|
1045
989
|
for (let k = start; k < end; k++) {
|
|
1046
990
|
htmls[k - start] = patches[k].html;
|
|
1047
991
|
}
|
|
1048
|
-
const {
|
|
992
|
+
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
|
|
1049
993
|
if (count !== htmls.length) {
|
|
1050
|
-
throw findOffendingInsert(patches, start, htmls);
|
|
994
|
+
throw findOffendingInsert(patches, start, htmls, liveParent);
|
|
1051
995
|
}
|
|
1052
|
-
const newNodes = collectTemplateChildren(
|
|
996
|
+
const newNodes = collectTemplateChildren(content, end - start);
|
|
1053
997
|
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
|
|
1054
|
-
liveParent.insertBefore(
|
|
998
|
+
liveParent.insertBefore(content, anchor);
|
|
1055
999
|
const newEntries = new Array(end - start);
|
|
1056
1000
|
for (let k = 0; k < newEntries.length; k++) {
|
|
1057
1001
|
const p = patches[start + k];
|
|
@@ -1067,18 +1011,18 @@ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
|
1067
1011
|
}
|
|
1068
1012
|
items.splice(startIdx, 0, ...newEntries);
|
|
1069
1013
|
}
|
|
1070
|
-
function findOffendingInsert(patches, start, htmls) {
|
|
1014
|
+
function findOffendingInsert(patches, start, htmls, liveParent) {
|
|
1071
1015
|
for (let i = 0; i < htmls.length; i++) {
|
|
1072
|
-
if (parseRowTemplate(htmls[i]).count !== 1) {
|
|
1073
|
-
return rowContractError(patches[start + i].index, htmls[i]);
|
|
1016
|
+
if (parseRowTemplate(htmls[i], liveParent).count !== 1) {
|
|
1017
|
+
return rowContractError(patches[start + i].index, htmls[i], liveParent);
|
|
1074
1018
|
}
|
|
1075
1019
|
}
|
|
1076
1020
|
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
|
|
1077
1021
|
}
|
|
1078
|
-
function findOffendingChange(patches, changes) {
|
|
1022
|
+
function findOffendingChange(patches, changes, liveParent) {
|
|
1079
1023
|
for (const c of changes) {
|
|
1080
|
-
if (parseRowTemplate(c.html).count !== 1) {
|
|
1081
|
-
return rowContractError(patches[c.patchIdx].index, c.html);
|
|
1024
|
+
if (parseRowTemplate(c.html, liveParent).count !== 1) {
|
|
1025
|
+
return rowContractError(patches[c.patchIdx].index, c.html, liveParent);
|
|
1082
1026
|
}
|
|
1083
1027
|
}
|
|
1084
1028
|
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
|
|
@@ -1101,7 +1045,7 @@ function tryInPlaceContentUpdate(binding, listSeg) {
|
|
|
1101
1045
|
}
|
|
1102
1046
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1103
1047
|
binding.items = newRecord;
|
|
1104
|
-
|
|
1048
|
+
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
|
|
1105
1049
|
return true;
|
|
1106
1050
|
}
|
|
1107
1051
|
function updateRowInPlace(liveParent, old, ni, index) {
|
|
@@ -1116,7 +1060,7 @@ function updateRowInPlace(liveParent, old, ni, index) {
|
|
|
1116
1060
|
bindingDisposers: kept.bindingDisposers
|
|
1117
1061
|
};
|
|
1118
1062
|
}
|
|
1119
|
-
const newNode = parseSingleRow(ni.html, index);
|
|
1063
|
+
const newNode = parseSingleRow(ni.html, index, liveParent);
|
|
1120
1064
|
if (old.node.tagName === newNode.tagName) {
|
|
1121
1065
|
_morphElement(old.node, newNode);
|
|
1122
1066
|
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
|
|
@@ -1148,14 +1092,14 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
1148
1092
|
const { liveParent } = binding;
|
|
1149
1093
|
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
|
|
1150
1094
|
const tailAnchor = endAnchor(binding);
|
|
1151
|
-
buildFreshNodes(newRecord, freshIndices, freshHtmls);
|
|
1095
|
+
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
|
|
1152
1096
|
const focusSnap = captureFocus(liveParent);
|
|
1153
1097
|
removeOldNodes(liveParent, removedItems);
|
|
1154
1098
|
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
|
|
1155
1099
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1156
1100
|
binding.items = newRecord;
|
|
1157
1101
|
if (newRecord.length > 0) {
|
|
1158
|
-
|
|
1102
|
+
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
|
|
1159
1103
|
}
|
|
1160
1104
|
}
|
|
1161
1105
|
function classifyItems(oldItems, listSeg) {
|
|
@@ -1196,13 +1140,13 @@ function classifyItems(oldItems, listSeg) {
|
|
|
1196
1140
|
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
|
|
1197
1141
|
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
|
|
1198
1142
|
}
|
|
1199
|
-
function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
1143
|
+
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
|
|
1200
1144
|
if (freshHtmls.length === 0) return;
|
|
1201
|
-
const {
|
|
1145
|
+
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
|
|
1202
1146
|
if (count !== freshHtmls.length) {
|
|
1203
|
-
throw findOffendingRow(newRecord, freshIndices, freshHtmls);
|
|
1147
|
+
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
|
|
1204
1148
|
}
|
|
1205
|
-
let node =
|
|
1149
|
+
let node = content.firstElementChild;
|
|
1206
1150
|
for (const idx of freshIndices) {
|
|
1207
1151
|
const next = node.nextElementSibling;
|
|
1208
1152
|
const item = newRecord[idx];
|
|
@@ -1213,10 +1157,10 @@ function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
|
1213
1157
|
node = next;
|
|
1214
1158
|
}
|
|
1215
1159
|
}
|
|
1216
|
-
function findOffendingRow(newRecord, freshIndices, freshHtmls) {
|
|
1160
|
+
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
|
|
1217
1161
|
for (let i = 0; i < freshHtmls.length; i++) {
|
|
1218
|
-
if (parseRowTemplate(freshHtmls[i]).count !== 1) {
|
|
1219
|
-
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html);
|
|
1162
|
+
if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) {
|
|
1163
|
+
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent);
|
|
1220
1164
|
}
|
|
1221
1165
|
}
|
|
1222
1166
|
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
|
|
@@ -1325,12 +1269,17 @@ function mount(rootEl, render) {
|
|
|
1325
1269
|
}
|
|
1326
1270
|
assertNotInsideMountedTree(rootEl);
|
|
1327
1271
|
setMounted(rootEl, true);
|
|
1328
|
-
const listenerWarnObserver =
|
|
1272
|
+
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
|
|
1329
1273
|
const bindings = /* @__PURE__ */ new Map();
|
|
1330
1274
|
const renderCtx = {
|
|
1331
1275
|
counter: 0,
|
|
1332
1276
|
caches: /* @__PURE__ */ new Map(),
|
|
1333
|
-
bindingCounts: /* @__PURE__ */ new Map()
|
|
1277
|
+
bindingCounts: /* @__PURE__ */ new Map(),
|
|
1278
|
+
bindingSources: /* @__PURE__ */ new Map(),
|
|
1279
|
+
keysThisRender: /* @__PURE__ */ new Set(),
|
|
1280
|
+
shiftCandidates: [],
|
|
1281
|
+
warnedShiftIds: /* @__PURE__ */ new Set(),
|
|
1282
|
+
rebuiltLists: /* @__PURE__ */ new Set()
|
|
1334
1283
|
};
|
|
1335
1284
|
const bindingCtx = newBindingContext();
|
|
1336
1285
|
let bindingDisposers = [];
|
|
@@ -1338,28 +1287,43 @@ function mount(rootEl, render) {
|
|
|
1338
1287
|
let isFirst = true;
|
|
1339
1288
|
let prevStaticHtml = "";
|
|
1340
1289
|
const valueOnlyWarnCtx = { warned: false };
|
|
1341
|
-
const
|
|
1290
|
+
const runRenderPass = () => {
|
|
1342
1291
|
renderCtx.counter = 0;
|
|
1292
|
+
renderCtx.keysThisRender.clear();
|
|
1293
|
+
renderCtx.shiftCandidates.length = 0;
|
|
1343
1294
|
bindingCtx.counter = 0;
|
|
1344
1295
|
bindingCtx.list = [];
|
|
1345
1296
|
_setRenderContext(renderCtx);
|
|
1346
1297
|
_setBindingContext(bindingCtx);
|
|
1347
|
-
let result;
|
|
1348
1298
|
try {
|
|
1349
|
-
|
|
1299
|
+
return render();
|
|
1350
1300
|
} finally {
|
|
1351
1301
|
_setRenderContext(null);
|
|
1352
1302
|
_setBindingContext(null);
|
|
1353
1303
|
}
|
|
1354
|
-
|
|
1304
|
+
};
|
|
1305
|
+
const disposeEffect = effect(() => {
|
|
1306
|
+
let result = runRenderPass();
|
|
1307
|
+
const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter;
|
|
1308
|
+
if (countChanged) {
|
|
1309
|
+
for (const id of renderCtx.shiftCandidates) {
|
|
1310
|
+
if (renderCtx.warnedShiftIds.has(id)) continue;
|
|
1311
|
+
renderCtx.warnedShiftIds.add(id);
|
|
1312
|
+
devHooks.listIdShift?.(id);
|
|
1313
|
+
}
|
|
1314
|
+
_resetCallOrderListState(renderCtx);
|
|
1315
|
+
result = runRenderPass();
|
|
1316
|
+
}
|
|
1317
|
+
let segment = resultToSegment(result);
|
|
1355
1318
|
if (isFirst) {
|
|
1356
1319
|
runFirstRender(rootEl, segment, bindings);
|
|
1357
1320
|
prevStaticHtml = flattenWithoutListItems(segment);
|
|
1321
|
+
devHooks.parserRepair?.(prevStaticHtml);
|
|
1358
1322
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1359
|
-
if (
|
|
1323
|
+
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
|
|
1360
1324
|
isFirst = false;
|
|
1361
1325
|
} else {
|
|
1362
|
-
|
|
1326
|
+
let nextStaticHtml = runSubsequentRender(
|
|
1363
1327
|
rootEl,
|
|
1364
1328
|
segment,
|
|
1365
1329
|
bindings,
|
|
@@ -1367,14 +1331,28 @@ function mount(rootEl, render) {
|
|
|
1367
1331
|
prevStaticHtml,
|
|
1368
1332
|
valueOnlyWarnCtx
|
|
1369
1333
|
);
|
|
1334
|
+
if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) {
|
|
1335
|
+
for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id);
|
|
1336
|
+
result = runRenderPass();
|
|
1337
|
+
segment = resultToSegment(result);
|
|
1338
|
+
nextStaticHtml = runSubsequentRender(
|
|
1339
|
+
rootEl,
|
|
1340
|
+
segment,
|
|
1341
|
+
bindings,
|
|
1342
|
+
renderCtx,
|
|
1343
|
+
prevStaticHtml,
|
|
1344
|
+
valueOnlyWarnCtx
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1370
1347
|
if (nextStaticHtml !== prevStaticHtml) {
|
|
1371
1348
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1372
|
-
if (
|
|
1349
|
+
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
|
|
1373
1350
|
} else {
|
|
1374
|
-
|
|
1351
|
+
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
|
|
1375
1352
|
}
|
|
1376
1353
|
prevStaticHtml = nextStaticHtml;
|
|
1377
1354
|
}
|
|
1355
|
+
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
|
|
1378
1356
|
for (const listSeg of collectLists(segment).values()) {
|
|
1379
1357
|
const binding = bindings.get(listSeg.id);
|
|
1380
1358
|
if (binding === void 0) {
|
|
@@ -1384,7 +1362,14 @@ function mount(rootEl, render) {
|
|
|
1384
1362
|
}
|
|
1385
1363
|
reconcileList(binding, listSeg);
|
|
1386
1364
|
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
1365
|
+
renderCtx.bindingSources.set(listSeg.id, listSeg.source);
|
|
1366
|
+
expectedCounts?.set(
|
|
1367
|
+
listSeg.id,
|
|
1368
|
+
listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length
|
|
1369
|
+
);
|
|
1387
1370
|
}
|
|
1371
|
+
renderCtx.previousCallCount = renderCtx.counter;
|
|
1372
|
+
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
|
|
1388
1373
|
});
|
|
1389
1374
|
return () => {
|
|
1390
1375
|
disposeEffect();
|
|
@@ -1402,16 +1387,17 @@ function runFirstRender(rootEl, segment, bindings) {
|
|
|
1402
1387
|
bindListsFromMarkers(rootEl, segment, bindings, true);
|
|
1403
1388
|
}
|
|
1404
1389
|
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
|
|
1390
|
+
renderCtx.rebuiltLists.clear();
|
|
1405
1391
|
const currentStaticHtml = flattenWithoutListItems(segment);
|
|
1406
1392
|
if (currentStaticHtml === prevStaticHtml) {
|
|
1407
1393
|
return prevStaticHtml;
|
|
1408
1394
|
}
|
|
1409
|
-
|
|
1395
|
+
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
|
|
1410
1396
|
cleanupOrphanBindings(segment, bindings, renderCtx);
|
|
1411
1397
|
const template = rootEl.cloneNode(false);
|
|
1412
1398
|
template.innerHTML = currentStaticHtml;
|
|
1413
1399
|
morph(rootEl, template, collectOwnedItems(bindings));
|
|
1414
|
-
bindListsFromMarkers(rootEl, segment, bindings, false);
|
|
1400
|
+
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
|
|
1415
1401
|
return currentStaticHtml;
|
|
1416
1402
|
}
|
|
1417
1403
|
function coerceRenderResult(result) {
|
|
@@ -1419,21 +1405,44 @@ function coerceRenderResult(result) {
|
|
|
1419
1405
|
if (result === false || result === true) return "";
|
|
1420
1406
|
return String(result);
|
|
1421
1407
|
}
|
|
1422
|
-
function
|
|
1408
|
+
function resultToSegment(result) {
|
|
1409
|
+
return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
|
|
1410
|
+
}
|
|
1411
|
+
function anyRebuiltListIsGranular(segment, rebuilt) {
|
|
1412
|
+
if (rebuilt.size === 0) return false;
|
|
1413
|
+
const lists = collectLists(segment);
|
|
1414
|
+
for (const id of rebuilt) {
|
|
1415
|
+
if (lists.get(id)?.patches !== void 0) return true;
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) {
|
|
1423
1420
|
const lists = collectLists(segment);
|
|
1424
1421
|
const found = [];
|
|
1425
1422
|
collectComments(rootEl, found);
|
|
1426
1423
|
for (const marker of found) {
|
|
1427
1424
|
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
|
|
1428
1425
|
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
|
|
1429
|
-
|
|
1426
|
+
const existing = bindings.get(id);
|
|
1427
|
+
if (existing !== void 0) {
|
|
1428
|
+
if (existing.marker === marker && rootEl.contains(existing.marker)) continue;
|
|
1429
|
+
for (const item of existing.items) {
|
|
1430
|
+
disposeRowBindings(item.bindingDisposers);
|
|
1431
|
+
if (rootEl.contains(item.node)) {
|
|
1432
|
+
item.node.parentElement?.removeChild(item.node);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
bindings.delete(id);
|
|
1436
|
+
rebuiltLists?.add(id);
|
|
1437
|
+
devHooks.listRebind?.(id, marker.parentElement);
|
|
1438
|
+
}
|
|
1430
1439
|
const listSeg = lists.get(id);
|
|
1431
1440
|
const liveParent = marker.parentElement;
|
|
1432
1441
|
const items = [];
|
|
1433
1442
|
if (inlinedItems) {
|
|
1434
1443
|
let next = marker.nextElementSibling;
|
|
1435
1444
|
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
|
|
1436
|
-
validateInlinedRowMatch(listSeg.items[i].html, i, next);
|
|
1445
|
+
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
|
|
1437
1446
|
const rowBindings = listSeg.items[i].bindings;
|
|
1438
1447
|
const bound = {
|
|
1439
1448
|
ref: listSeg.items[i].ref,
|
|
@@ -1451,17 +1460,25 @@ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
|
1451
1460
|
}
|
|
1452
1461
|
const binding = { liveParent, items, marker };
|
|
1453
1462
|
if (items.length > 0) {
|
|
1454
|
-
|
|
1463
|
+
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
|
|
1455
1464
|
}
|
|
1456
|
-
|
|
1465
|
+
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
|
|
1457
1466
|
bindings.set(id, binding);
|
|
1458
1467
|
}
|
|
1459
1468
|
}
|
|
1460
|
-
function validateInlinedRowMatch(expectedHtml, index, boundEl) {
|
|
1469
|
+
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
|
|
1461
1470
|
if (boundEl.outerHTML === expectedHtml) return;
|
|
1462
|
-
const { count } = parseRowTemplate(expectedHtml);
|
|
1463
|
-
if (count
|
|
1464
|
-
|
|
1471
|
+
const { content, count } = parseRowTemplate(expectedHtml, liveParent);
|
|
1472
|
+
if (count !== 1) throw rowContractError(index, expectedHtml, liveParent);
|
|
1473
|
+
const expectedTag = content.firstElementChild.tagName;
|
|
1474
|
+
if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag);
|
|
1475
|
+
}
|
|
1476
|
+
function rowStructureError(index, gotTag, wantTag) {
|
|
1477
|
+
const got = gotTag.toLowerCase();
|
|
1478
|
+
const want = wantTag.toLowerCase();
|
|
1479
|
+
return new Error(
|
|
1480
|
+
`each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.`
|
|
1481
|
+
);
|
|
1465
1482
|
}
|
|
1466
1483
|
function collectOwnedItems(bindings) {
|
|
1467
1484
|
const owned = /* @__PURE__ */ new Set();
|
|
@@ -1485,6 +1502,7 @@ function cleanupOrphanBindings(segment, bindings, renderCtx) {
|
|
|
1485
1502
|
}
|
|
1486
1503
|
bindings.delete(id);
|
|
1487
1504
|
renderCtx.bindingCounts.delete(id);
|
|
1505
|
+
renderCtx.bindingSources.delete(id);
|
|
1488
1506
|
renderCtx.caches.delete(id);
|
|
1489
1507
|
}
|
|
1490
1508
|
}
|