kerfjs 2.0.1 → 3.0.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 +100 -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-GYRZQCSY.js → chunk-JVVU2RQO.js} +11 -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 +351 -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-GYRZQCSY.js.map +0 -1
- 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/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,12 @@ 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
|
+
);
|
|
389
492
|
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
|
|
390
493
|
const owned = boundTextNodeOf(matched);
|
|
391
494
|
if (owned !== null && fromChild === owned) {
|
|
@@ -393,6 +496,35 @@ function morphChildren(fromParent, toParent, ownedItems) {
|
|
|
393
496
|
}
|
|
394
497
|
}
|
|
395
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
|
+
}
|
|
527
|
+
}
|
|
396
528
|
if (matched !== null) {
|
|
397
529
|
morphNode(matched, toChild, ownedItems);
|
|
398
530
|
} else {
|
|
@@ -499,144 +631,12 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
499
631
|
}
|
|
500
632
|
}
|
|
501
633
|
|
|
502
|
-
// src/dev-binding-warn.ts
|
|
503
|
-
var warnedHoles = /* @__PURE__ */ new Set();
|
|
504
|
-
function isOptedIn2() {
|
|
505
|
-
if (!isDevMode()) return false;
|
|
506
|
-
const proc = globalThis.process;
|
|
507
|
-
return proc?.env?.KERF_DEV_WARN_STALE_BINDING === "1";
|
|
508
|
-
}
|
|
509
|
-
function describeHole(b) {
|
|
510
|
-
return b.kind === "attr" ? `attr '${b.attr}' (id '${b.id}')` : `text (id '${b.id}')`;
|
|
511
|
-
}
|
|
512
|
-
function maybeWarnStaleBinding(prevWired, current) {
|
|
513
|
-
if (!isOptedIn2()) return;
|
|
514
|
-
const n = Math.min(prevWired.length, current.length);
|
|
515
|
-
for (let i = 0; i < n; i++) {
|
|
516
|
-
const cur = current[i];
|
|
517
|
-
if (prevWired[i].signal === cur.signal) continue;
|
|
518
|
-
if (warnedHoles.has(cur.id)) continue;
|
|
519
|
-
warnedHoles.add(cur.id);
|
|
520
|
-
console.warn(
|
|
521
|
-
`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.`
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/dev-listener-warn.ts
|
|
527
|
-
var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
|
|
528
|
-
var patched = false;
|
|
529
|
-
var warned = false;
|
|
530
|
-
function isOptedIn3() {
|
|
531
|
-
if (!isDevMode()) return false;
|
|
532
|
-
const proc = globalThis.process;
|
|
533
|
-
return proc?.env?.KERF_DEV_WARN_REBUILT_LISTENERS === "1";
|
|
534
|
-
}
|
|
535
|
-
function findAddEventListenerProto() {
|
|
536
|
-
const probe = document.createElement("div");
|
|
537
|
-
let proto = Object.getPrototypeOf(probe);
|
|
538
|
-
while (!Object.prototype.hasOwnProperty.call(proto, "addEventListener")) {
|
|
539
|
-
proto = Object.getPrototypeOf(proto);
|
|
540
|
-
}
|
|
541
|
-
return proto;
|
|
542
|
-
}
|
|
543
|
-
function patchAddEventListenerOnce() {
|
|
544
|
-
if (patched) return;
|
|
545
|
-
patched = true;
|
|
546
|
-
const proto = findAddEventListenerProto();
|
|
547
|
-
const orig = proto.addEventListener;
|
|
548
|
-
proto.addEventListener = function(type, listener, options) {
|
|
549
|
-
if (this instanceof Element) {
|
|
550
|
-
this[LISTENER_MARKER] = true;
|
|
551
|
-
}
|
|
552
|
-
return orig.call(this, type, listener, options);
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function hasMarkedListener(el) {
|
|
556
|
-
if (el[LISTENER_MARKER] === true) return true;
|
|
557
|
-
const stack = [];
|
|
558
|
-
for (let i = 0; i < el.children.length; i++) stack.push(el.children[i]);
|
|
559
|
-
while (stack.length > 0) {
|
|
560
|
-
const cur = stack.pop();
|
|
561
|
-
if (cur[LISTENER_MARKER] === true) return true;
|
|
562
|
-
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
|
|
563
|
-
}
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
function emitWarning() {
|
|
567
|
-
if (warned) return;
|
|
568
|
-
warned = true;
|
|
569
|
-
console.warn(
|
|
570
|
-
"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."
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
function installListenerRebuildWarn(rootEl) {
|
|
574
|
-
if (!isOptedIn3()) return null;
|
|
575
|
-
patchAddEventListenerOnce();
|
|
576
|
-
const observer = new MutationObserver((mutations) => {
|
|
577
|
-
if (warned) return;
|
|
578
|
-
for (const m of mutations) {
|
|
579
|
-
for (let i = 0; i < m.removedNodes.length; i++) {
|
|
580
|
-
const removed = m.removedNodes[i];
|
|
581
|
-
if (!(removed instanceof Element)) continue;
|
|
582
|
-
if (hasMarkedListener(removed)) {
|
|
583
|
-
emitWarning();
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
observer.observe(rootEl, { childList: true, subtree: true });
|
|
590
|
-
return observer;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// src/dev-rerender-warn.ts
|
|
594
|
-
function isOptedIn4() {
|
|
595
|
-
const proc = globalThis.process;
|
|
596
|
-
if (proc?.env?.KERF_DEV_WARN_VALUE_ONLY_RERENDER !== "1") return false;
|
|
597
|
-
return isDevMode();
|
|
598
|
-
}
|
|
599
|
-
var ELEMENT_NODE2 = 1;
|
|
600
|
-
var TEXT_NODE2 = 3;
|
|
601
|
-
var COMMENT_NODE2 = 8;
|
|
602
|
-
function _isValueOnlyDiff(a, b) {
|
|
603
|
-
const an = a.childNodes;
|
|
604
|
-
const bn = b.childNodes;
|
|
605
|
-
if (an.length !== bn.length) return false;
|
|
606
|
-
for (let i = 0; i < an.length; i++) {
|
|
607
|
-
const x = an[i];
|
|
608
|
-
const y = bn[i];
|
|
609
|
-
if (x.nodeType !== y.nodeType) return false;
|
|
610
|
-
if (x.nodeType === ELEMENT_NODE2) {
|
|
611
|
-
if (x.tagName !== y.tagName) return false;
|
|
612
|
-
if (!_isValueOnlyDiff(x, y)) return false;
|
|
613
|
-
} else if (x.nodeType === COMMENT_NODE2) {
|
|
614
|
-
if (x.data !== y.data) return false;
|
|
615
|
-
} else if (x.nodeType !== TEXT_NODE2) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
function maybeWarnValueOnlyRerender(prevHtml, nextHtml, ctx) {
|
|
622
|
-
if (ctx.warned || !isOptedIn4()) return;
|
|
623
|
-
const a = document.createElement("template");
|
|
624
|
-
const b = document.createElement("template");
|
|
625
|
-
a.innerHTML = prevHtml;
|
|
626
|
-
b.innerHTML = nextHtml;
|
|
627
|
-
if (!_isValueOnlyDiff(a.content, b.content)) return;
|
|
628
|
-
ctx.warned = true;
|
|
629
|
-
console.warn(
|
|
630
|
-
"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."
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
634
|
// src/list-binding.ts
|
|
635
635
|
function endAnchor(binding) {
|
|
636
636
|
if (binding.items.length > 0) {
|
|
637
|
-
return binding.items[binding.items.length - 1].node.
|
|
637
|
+
return binding.items[binding.items.length - 1].node.nextSibling;
|
|
638
638
|
}
|
|
639
|
-
return binding.marker.
|
|
639
|
+
return binding.marker.nextSibling;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
642
|
// src/list-reconcile-fast-paths.ts
|
|
@@ -647,8 +647,8 @@ var SQUOTE = 39;
|
|
|
647
647
|
var AMP = 38;
|
|
648
648
|
var EQ = 61;
|
|
649
649
|
var SLASH = 47;
|
|
650
|
-
var
|
|
651
|
-
var
|
|
650
|
+
var TEXT_NODE2 = 3;
|
|
651
|
+
var ELEMENT_NODE2 = 1;
|
|
652
652
|
function isWhitespace(cc) {
|
|
653
653
|
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
|
|
654
654
|
}
|
|
@@ -673,12 +673,15 @@ function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
|
|
|
673
673
|
for (const [name, rawValue] of newTag.attrs) {
|
|
674
674
|
const oldValue = oldTag.attrs.get(name);
|
|
675
675
|
if (oldValue === rawValue) continue;
|
|
676
|
-
|
|
676
|
+
const value = unescapeAttrValue(rawValue);
|
|
677
|
+
liveNode.setAttribute(name, value);
|
|
678
|
+
syncFormProp(liveNode, name, value, true);
|
|
677
679
|
}
|
|
678
680
|
for (const name of oldTag.attrs.keys()) {
|
|
679
681
|
if (newTag.attrs.has(name)) continue;
|
|
680
682
|
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
|
|
681
683
|
liveNode.removeAttribute(name);
|
|
684
|
+
syncFormProp(liveNode, name, "", false);
|
|
682
685
|
}
|
|
683
686
|
return true;
|
|
684
687
|
}
|
|
@@ -713,6 +716,10 @@ function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
|
|
|
713
716
|
if (targetNode === null) return false;
|
|
714
717
|
if (targetNode.nodeValue !== oldText) return false;
|
|
715
718
|
targetNode.nodeValue = newText;
|
|
719
|
+
const host = targetNode.parentNode;
|
|
720
|
+
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
|
|
721
|
+
host.value = newText;
|
|
722
|
+
}
|
|
716
723
|
return true;
|
|
717
724
|
}
|
|
718
725
|
function containsDataMorphSkip(html) {
|
|
@@ -752,13 +759,13 @@ function nthTextNodeDescendant(root, n) {
|
|
|
752
759
|
function walk(node) {
|
|
753
760
|
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
754
761
|
if (result !== null) return;
|
|
755
|
-
if (c.nodeType ===
|
|
762
|
+
if (c.nodeType === TEXT_NODE2) {
|
|
756
763
|
if (count === n) {
|
|
757
764
|
result = c;
|
|
758
765
|
return;
|
|
759
766
|
}
|
|
760
767
|
count++;
|
|
761
|
-
} else if (c.nodeType ===
|
|
768
|
+
} else if (c.nodeType === ELEMENT_NODE2) {
|
|
762
769
|
walk(c);
|
|
763
770
|
}
|
|
764
771
|
}
|
|
@@ -818,76 +825,6 @@ function isUserAgentOwnedAttr2(tagNameUpper, name) {
|
|
|
818
825
|
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
|
|
819
826
|
}
|
|
820
827
|
|
|
821
|
-
// src/list-reconcile-focus.ts
|
|
822
|
-
function captureFocus(liveParent) {
|
|
823
|
-
const active = document.activeElement;
|
|
824
|
-
if (active === null || active === document.body) return null;
|
|
825
|
-
if (!liveParent.contains(active)) return null;
|
|
826
|
-
const el = active;
|
|
827
|
-
let selStart = null;
|
|
828
|
-
let selEnd = null;
|
|
829
|
-
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
830
|
-
try {
|
|
831
|
-
selStart = el.selectionStart;
|
|
832
|
-
selEnd = el.selectionEnd;
|
|
833
|
-
} catch {
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
return { el, selStart, selEnd };
|
|
837
|
-
}
|
|
838
|
-
function restoreFocus(snap) {
|
|
839
|
-
if (document.activeElement === snap.el) return;
|
|
840
|
-
if (!snap.el.isConnected) return;
|
|
841
|
-
snap.el.focus();
|
|
842
|
-
if (snap.selStart !== null && snap.selEnd !== null) {
|
|
843
|
-
try {
|
|
844
|
-
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
|
|
845
|
-
} catch {
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// src/utils/rowContract.ts
|
|
851
|
-
var ROW_HTML_SNIPPET_MAX = 120;
|
|
852
|
-
function truncateRowHtml(html) {
|
|
853
|
-
return html.length > ROW_HTML_SNIPPET_MAX ? html.slice(0, ROW_HTML_SNIPPET_MAX) + "\u2026" : html;
|
|
854
|
-
}
|
|
855
|
-
function parseRowTemplate(html) {
|
|
856
|
-
const tpl = document.createElement("template");
|
|
857
|
-
tpl.innerHTML = html;
|
|
858
|
-
return { tpl, count: tpl.content.children.length };
|
|
859
|
-
}
|
|
860
|
-
function parseSingleRow(html, index) {
|
|
861
|
-
const { tpl, count } = parseRowTemplate(html);
|
|
862
|
-
if (count !== 1) throw rowContractError(index, html);
|
|
863
|
-
return tpl.content.firstElementChild;
|
|
864
|
-
}
|
|
865
|
-
function collectTemplateChildren(tpl, n) {
|
|
866
|
-
const nodes = new Array(n);
|
|
867
|
-
let child = tpl.content.firstElementChild;
|
|
868
|
-
for (let k = 0; k < n; k++) {
|
|
869
|
-
nodes[k] = child;
|
|
870
|
-
child = child.nextElementSibling;
|
|
871
|
-
}
|
|
872
|
-
return nodes;
|
|
873
|
-
}
|
|
874
|
-
function rowContractError(index, html) {
|
|
875
|
-
const { count } = parseRowTemplate(html);
|
|
876
|
-
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
877
|
-
return new Error(
|
|
878
|
-
`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))}`
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
function maybeWarnMissingRowKey(rowEl, rowHtml, binding) {
|
|
882
|
-
if (!isDevMode()) return;
|
|
883
|
-
if (binding.warnedMissingKey === true) return;
|
|
884
|
-
binding.warnedMissingKey = true;
|
|
885
|
-
if (rowEl.id !== "" || rowEl.hasAttribute("data-key")) return;
|
|
886
|
-
console.warn(
|
|
887
|
-
`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))}`
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
828
|
// src/list-reconcile-granular.ts
|
|
892
829
|
function reconcileGranular(binding, patches) {
|
|
893
830
|
const { liveParent } = binding;
|
|
@@ -950,12 +887,12 @@ function reconcileGranular(binding, patches) {
|
|
|
950
887
|
}
|
|
951
888
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
952
889
|
if (items.length > 0) {
|
|
953
|
-
|
|
890
|
+
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
|
|
954
891
|
}
|
|
955
892
|
}
|
|
956
893
|
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
957
894
|
const { html } = patch;
|
|
958
|
-
const newNode = parseSingleRow(html, patch.index);
|
|
895
|
+
const newNode = parseSingleRow(html, patch.index, liveParent);
|
|
959
896
|
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
|
|
960
897
|
liveParent.insertBefore(newNode, anchor);
|
|
961
898
|
items.splice(patch.index, 0, {
|
|
@@ -982,7 +919,7 @@ function applySingleUpdate(liveParent, items, patch) {
|
|
|
982
919
|
items[patch.index] = reuseBound(patch, html, oldEntry);
|
|
983
920
|
return;
|
|
984
921
|
}
|
|
985
|
-
const newNode = parseSingleRow(html, patch.index);
|
|
922
|
+
const newNode = parseSingleRow(html, patch.index, liveParent);
|
|
986
923
|
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
|
|
987
924
|
}
|
|
988
925
|
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
|
|
@@ -1035,11 +972,11 @@ function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
|
1035
972
|
morphChanges.push({ patchIdx: k, html: p.html });
|
|
1036
973
|
}
|
|
1037
974
|
if (morphChanges.length === 0) return;
|
|
1038
|
-
const {
|
|
975
|
+
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
|
|
1039
976
|
if (count !== morphChanges.length) {
|
|
1040
|
-
throw findOffendingChange(patches, morphChanges);
|
|
977
|
+
throw findOffendingChange(patches, morphChanges, liveParent);
|
|
1041
978
|
}
|
|
1042
|
-
const newNodes = collectTemplateChildren(
|
|
979
|
+
const newNodes = collectTemplateChildren(content, morphChanges.length);
|
|
1043
980
|
for (let k = 0; k < morphChanges.length; k++) {
|
|
1044
981
|
const c = morphChanges[k];
|
|
1045
982
|
const p = patches[c.patchIdx];
|
|
@@ -1052,13 +989,13 @@ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
|
1052
989
|
for (let k = start; k < end; k++) {
|
|
1053
990
|
htmls[k - start] = patches[k].html;
|
|
1054
991
|
}
|
|
1055
|
-
const {
|
|
992
|
+
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
|
|
1056
993
|
if (count !== htmls.length) {
|
|
1057
|
-
throw findOffendingInsert(patches, start, htmls);
|
|
994
|
+
throw findOffendingInsert(patches, start, htmls, liveParent);
|
|
1058
995
|
}
|
|
1059
|
-
const newNodes = collectTemplateChildren(
|
|
996
|
+
const newNodes = collectTemplateChildren(content, end - start);
|
|
1060
997
|
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
|
|
1061
|
-
liveParent.insertBefore(
|
|
998
|
+
liveParent.insertBefore(content, anchor);
|
|
1062
999
|
const newEntries = new Array(end - start);
|
|
1063
1000
|
for (let k = 0; k < newEntries.length; k++) {
|
|
1064
1001
|
const p = patches[start + k];
|
|
@@ -1074,18 +1011,18 @@ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
|
1074
1011
|
}
|
|
1075
1012
|
items.splice(startIdx, 0, ...newEntries);
|
|
1076
1013
|
}
|
|
1077
|
-
function findOffendingInsert(patches, start, htmls) {
|
|
1014
|
+
function findOffendingInsert(patches, start, htmls, liveParent) {
|
|
1078
1015
|
for (let i = 0; i < htmls.length; i++) {
|
|
1079
|
-
if (parseRowTemplate(htmls[i]).count !== 1) {
|
|
1080
|
-
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);
|
|
1081
1018
|
}
|
|
1082
1019
|
}
|
|
1083
1020
|
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
|
|
1084
1021
|
}
|
|
1085
|
-
function findOffendingChange(patches, changes) {
|
|
1022
|
+
function findOffendingChange(patches, changes, liveParent) {
|
|
1086
1023
|
for (const c of changes) {
|
|
1087
|
-
if (parseRowTemplate(c.html).count !== 1) {
|
|
1088
|
-
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);
|
|
1089
1026
|
}
|
|
1090
1027
|
}
|
|
1091
1028
|
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
|
|
@@ -1108,7 +1045,7 @@ function tryInPlaceContentUpdate(binding, listSeg) {
|
|
|
1108
1045
|
}
|
|
1109
1046
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1110
1047
|
binding.items = newRecord;
|
|
1111
|
-
|
|
1048
|
+
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
|
|
1112
1049
|
return true;
|
|
1113
1050
|
}
|
|
1114
1051
|
function updateRowInPlace(liveParent, old, ni, index) {
|
|
@@ -1123,7 +1060,7 @@ function updateRowInPlace(liveParent, old, ni, index) {
|
|
|
1123
1060
|
bindingDisposers: kept.bindingDisposers
|
|
1124
1061
|
};
|
|
1125
1062
|
}
|
|
1126
|
-
const newNode = parseSingleRow(ni.html, index);
|
|
1063
|
+
const newNode = parseSingleRow(ni.html, index, liveParent);
|
|
1127
1064
|
if (old.node.tagName === newNode.tagName) {
|
|
1128
1065
|
_morphElement(old.node, newNode);
|
|
1129
1066
|
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
|
|
@@ -1155,14 +1092,14 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
1155
1092
|
const { liveParent } = binding;
|
|
1156
1093
|
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
|
|
1157
1094
|
const tailAnchor = endAnchor(binding);
|
|
1158
|
-
buildFreshNodes(newRecord, freshIndices, freshHtmls);
|
|
1095
|
+
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
|
|
1159
1096
|
const focusSnap = captureFocus(liveParent);
|
|
1160
1097
|
removeOldNodes(liveParent, removedItems);
|
|
1161
1098
|
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
|
|
1162
1099
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1163
1100
|
binding.items = newRecord;
|
|
1164
1101
|
if (newRecord.length > 0) {
|
|
1165
|
-
|
|
1102
|
+
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
|
|
1166
1103
|
}
|
|
1167
1104
|
}
|
|
1168
1105
|
function classifyItems(oldItems, listSeg) {
|
|
@@ -1203,13 +1140,13 @@ function classifyItems(oldItems, listSeg) {
|
|
|
1203
1140
|
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
|
|
1204
1141
|
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
|
|
1205
1142
|
}
|
|
1206
|
-
function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
1143
|
+
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
|
|
1207
1144
|
if (freshHtmls.length === 0) return;
|
|
1208
|
-
const {
|
|
1145
|
+
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
|
|
1209
1146
|
if (count !== freshHtmls.length) {
|
|
1210
|
-
throw findOffendingRow(newRecord, freshIndices, freshHtmls);
|
|
1147
|
+
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
|
|
1211
1148
|
}
|
|
1212
|
-
let node =
|
|
1149
|
+
let node = content.firstElementChild;
|
|
1213
1150
|
for (const idx of freshIndices) {
|
|
1214
1151
|
const next = node.nextElementSibling;
|
|
1215
1152
|
const item = newRecord[idx];
|
|
@@ -1220,10 +1157,10 @@ function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
|
|
|
1220
1157
|
node = next;
|
|
1221
1158
|
}
|
|
1222
1159
|
}
|
|
1223
|
-
function findOffendingRow(newRecord, freshIndices, freshHtmls) {
|
|
1160
|
+
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
|
|
1224
1161
|
for (let i = 0; i < freshHtmls.length; i++) {
|
|
1225
|
-
if (parseRowTemplate(freshHtmls[i]).count !== 1) {
|
|
1226
|
-
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);
|
|
1227
1164
|
}
|
|
1228
1165
|
}
|
|
1229
1166
|
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
|
|
@@ -1332,12 +1269,17 @@ function mount(rootEl, render) {
|
|
|
1332
1269
|
}
|
|
1333
1270
|
assertNotInsideMountedTree(rootEl);
|
|
1334
1271
|
setMounted(rootEl, true);
|
|
1335
|
-
const listenerWarnObserver =
|
|
1272
|
+
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
|
|
1336
1273
|
const bindings = /* @__PURE__ */ new Map();
|
|
1337
1274
|
const renderCtx = {
|
|
1338
1275
|
counter: 0,
|
|
1339
1276
|
caches: /* @__PURE__ */ new Map(),
|
|
1340
|
-
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()
|
|
1341
1283
|
};
|
|
1342
1284
|
const bindingCtx = newBindingContext();
|
|
1343
1285
|
let bindingDisposers = [];
|
|
@@ -1345,28 +1287,43 @@ function mount(rootEl, render) {
|
|
|
1345
1287
|
let isFirst = true;
|
|
1346
1288
|
let prevStaticHtml = "";
|
|
1347
1289
|
const valueOnlyWarnCtx = { warned: false };
|
|
1348
|
-
const
|
|
1290
|
+
const runRenderPass = () => {
|
|
1349
1291
|
renderCtx.counter = 0;
|
|
1292
|
+
renderCtx.keysThisRender.clear();
|
|
1293
|
+
renderCtx.shiftCandidates.length = 0;
|
|
1350
1294
|
bindingCtx.counter = 0;
|
|
1351
1295
|
bindingCtx.list = [];
|
|
1352
1296
|
_setRenderContext(renderCtx);
|
|
1353
1297
|
_setBindingContext(bindingCtx);
|
|
1354
|
-
let result;
|
|
1355
1298
|
try {
|
|
1356
|
-
|
|
1299
|
+
return render();
|
|
1357
1300
|
} finally {
|
|
1358
1301
|
_setRenderContext(null);
|
|
1359
1302
|
_setBindingContext(null);
|
|
1360
1303
|
}
|
|
1361
|
-
|
|
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);
|
|
1362
1318
|
if (isFirst) {
|
|
1363
1319
|
runFirstRender(rootEl, segment, bindings);
|
|
1364
1320
|
prevStaticHtml = flattenWithoutListItems(segment);
|
|
1321
|
+
devHooks.parserRepair?.(prevStaticHtml);
|
|
1365
1322
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1366
|
-
if (
|
|
1323
|
+
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
|
|
1367
1324
|
isFirst = false;
|
|
1368
1325
|
} else {
|
|
1369
|
-
|
|
1326
|
+
let nextStaticHtml = runSubsequentRender(
|
|
1370
1327
|
rootEl,
|
|
1371
1328
|
segment,
|
|
1372
1329
|
bindings,
|
|
@@ -1374,14 +1331,28 @@ function mount(rootEl, render) {
|
|
|
1374
1331
|
prevStaticHtml,
|
|
1375
1332
|
valueOnlyWarnCtx
|
|
1376
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
|
+
}
|
|
1377
1347
|
if (nextStaticHtml !== prevStaticHtml) {
|
|
1378
1348
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1379
|
-
if (
|
|
1349
|
+
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
|
|
1380
1350
|
} else {
|
|
1381
|
-
|
|
1351
|
+
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
|
|
1382
1352
|
}
|
|
1383
1353
|
prevStaticHtml = nextStaticHtml;
|
|
1384
1354
|
}
|
|
1355
|
+
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
|
|
1385
1356
|
for (const listSeg of collectLists(segment).values()) {
|
|
1386
1357
|
const binding = bindings.get(listSeg.id);
|
|
1387
1358
|
if (binding === void 0) {
|
|
@@ -1391,7 +1362,14 @@ function mount(rootEl, render) {
|
|
|
1391
1362
|
}
|
|
1392
1363
|
reconcileList(binding, listSeg);
|
|
1393
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
|
+
);
|
|
1394
1370
|
}
|
|
1371
|
+
renderCtx.previousCallCount = renderCtx.counter;
|
|
1372
|
+
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
|
|
1395
1373
|
});
|
|
1396
1374
|
return () => {
|
|
1397
1375
|
disposeEffect();
|
|
@@ -1409,16 +1387,17 @@ function runFirstRender(rootEl, segment, bindings) {
|
|
|
1409
1387
|
bindListsFromMarkers(rootEl, segment, bindings, true);
|
|
1410
1388
|
}
|
|
1411
1389
|
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
|
|
1390
|
+
renderCtx.rebuiltLists.clear();
|
|
1412
1391
|
const currentStaticHtml = flattenWithoutListItems(segment);
|
|
1413
1392
|
if (currentStaticHtml === prevStaticHtml) {
|
|
1414
1393
|
return prevStaticHtml;
|
|
1415
1394
|
}
|
|
1416
|
-
|
|
1395
|
+
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
|
|
1417
1396
|
cleanupOrphanBindings(segment, bindings, renderCtx);
|
|
1418
1397
|
const template = rootEl.cloneNode(false);
|
|
1419
1398
|
template.innerHTML = currentStaticHtml;
|
|
1420
1399
|
morph(rootEl, template, collectOwnedItems(bindings));
|
|
1421
|
-
bindListsFromMarkers(rootEl, segment, bindings, false);
|
|
1400
|
+
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
|
|
1422
1401
|
return currentStaticHtml;
|
|
1423
1402
|
}
|
|
1424
1403
|
function coerceRenderResult(result) {
|
|
@@ -1426,21 +1405,44 @@ function coerceRenderResult(result) {
|
|
|
1426
1405
|
if (result === false || result === true) return "";
|
|
1427
1406
|
return String(result);
|
|
1428
1407
|
}
|
|
1429
|
-
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) {
|
|
1430
1420
|
const lists = collectLists(segment);
|
|
1431
1421
|
const found = [];
|
|
1432
1422
|
collectComments(rootEl, found);
|
|
1433
1423
|
for (const marker of found) {
|
|
1434
1424
|
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
|
|
1435
1425
|
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
|
|
1436
|
-
|
|
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
|
+
}
|
|
1437
1439
|
const listSeg = lists.get(id);
|
|
1438
1440
|
const liveParent = marker.parentElement;
|
|
1439
1441
|
const items = [];
|
|
1440
1442
|
if (inlinedItems) {
|
|
1441
1443
|
let next = marker.nextElementSibling;
|
|
1442
1444
|
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
|
|
1443
|
-
validateInlinedRowMatch(listSeg.items[i].html, i, next);
|
|
1445
|
+
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
|
|
1444
1446
|
const rowBindings = listSeg.items[i].bindings;
|
|
1445
1447
|
const bound = {
|
|
1446
1448
|
ref: listSeg.items[i].ref,
|
|
@@ -1458,17 +1460,25 @@ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
|
1458
1460
|
}
|
|
1459
1461
|
const binding = { liveParent, items, marker };
|
|
1460
1462
|
if (items.length > 0) {
|
|
1461
|
-
|
|
1463
|
+
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
|
|
1462
1464
|
}
|
|
1463
|
-
|
|
1465
|
+
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
|
|
1464
1466
|
bindings.set(id, binding);
|
|
1465
1467
|
}
|
|
1466
1468
|
}
|
|
1467
|
-
function validateInlinedRowMatch(expectedHtml, index, boundEl) {
|
|
1469
|
+
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
|
|
1468
1470
|
if (boundEl.outerHTML === expectedHtml) return;
|
|
1469
|
-
const { count } = parseRowTemplate(expectedHtml);
|
|
1470
|
-
if (count
|
|
1471
|
-
|
|
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
|
+
);
|
|
1472
1482
|
}
|
|
1473
1483
|
function collectOwnedItems(bindings) {
|
|
1474
1484
|
const owned = /* @__PURE__ */ new Set();
|
|
@@ -1492,6 +1502,7 @@ function cleanupOrphanBindings(segment, bindings, renderCtx) {
|
|
|
1492
1502
|
}
|
|
1493
1503
|
bindings.delete(id);
|
|
1494
1504
|
renderCtx.bindingCounts.delete(id);
|
|
1505
|
+
renderCtx.bindingSources.delete(id);
|
|
1495
1506
|
renderCtx.caches.delete(id);
|
|
1496
1507
|
}
|
|
1497
1508
|
}
|