kerfjs 1.0.1 → 2.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 +32 -0
- package/LICENSE +29 -1
- package/README.md +45 -17
- package/ai/cursorrules +21 -6
- package/ai/manifest.json +5 -5
- package/ai/skill.md +23 -6
- package/dist/array-signal.js +1 -1
- package/dist/chunk-KFUDM3VP.js +131 -0
- package/dist/chunk-KFUDM3VP.js.map +1 -0
- package/dist/{chunk-4E26PO2C.js → chunk-NU7YHYEV.js} +13 -5
- package/dist/chunk-NU7YHYEV.js.map +1 -0
- package/dist/{chunk-QNYOMGI4.js → chunk-RYZHZBHE.js} +74 -17
- package/dist/chunk-RYZHZBHE.js.map +1 -0
- package/dist/html.d.ts +50 -0
- package/dist/html.js +156 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +70 -34
- package/dist/index.js +232 -126
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +96 -55
- package/dist/jsx-runtime.js +2 -2
- package/dist/testing.js +2 -2
- package/llms.txt +6 -5
- package/package.json +7 -2
- package/dist/chunk-4E26PO2C.js.map +0 -1
- package/dist/chunk-7SKIIA5P.js +0 -70
- package/dist/chunk-7SKIIA5P.js.map +0 -1
- package/dist/chunk-QNYOMGI4.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { captureRowBindings, isSafeHtml, listSafeHtml, newBindingContext, _setBindingContext, flattenWithoutListItems, wireBindings, collectLists, disposeRowBindings, flatten, wireRowBindings, granularListSafeHtml } from './chunk-
|
|
2
|
-
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-
|
|
3
|
-
export { defineStore, resetAllStores } from './chunk-
|
|
4
|
-
import { warnIfInsideEffect, effect } from './chunk-
|
|
5
|
-
export { batch, computed, effect, signal } from './chunk-
|
|
1
|
+
import { captureRowBindings, isSafeHtml, listSafeHtml, syncFormProp, newBindingContext, _setBindingContext, flattenWithoutListItems, wireBindings, collectLists, disposeRowBindings, flatten, LIST_MARKER_PREFIX, wireRowBindings, granularListSafeHtml, carryOrRewireRowBindings } from './chunk-RYZHZBHE.js';
|
|
2
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-RYZHZBHE.js';
|
|
3
|
+
export { defineStore, resetAllStores } from './chunk-KFUDM3VP.js';
|
|
4
|
+
import { warnIfInsideEffect, effect, isDevMode } from './chunk-NU7YHYEV.js';
|
|
5
|
+
export { batch, computed, effect, signal } from './chunk-NU7YHYEV.js';
|
|
6
6
|
|
|
7
7
|
// src/attrSelector.ts
|
|
8
8
|
function cssEscapeIdent(value) {
|
|
@@ -94,33 +94,30 @@ function assertValidSelector(selector, fn) {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
warnIfInsideEffect("delegate");
|
|
100
|
-
const listener = (event) => {
|
|
97
|
+
function makeListener(rootEl, selector, handler, match) {
|
|
98
|
+
return (event) => {
|
|
101
99
|
const target = event.target;
|
|
102
100
|
if (!(target instanceof Element)) return;
|
|
103
|
-
const matched = target.closest(selector);
|
|
101
|
+
const matched = match === "direct" ? target.matches(selector) ? target : null : target.closest(selector);
|
|
104
102
|
if (matched !== null && rootEl.contains(matched)) {
|
|
105
103
|
handler(event, matched);
|
|
106
104
|
}
|
|
107
105
|
};
|
|
106
|
+
}
|
|
107
|
+
function delegate(rootEl, type, selector, handler, options) {
|
|
108
|
+
assertValidSelector(selector, "delegate");
|
|
109
|
+
warnIfInsideEffect("delegate");
|
|
110
|
+
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
|
|
108
111
|
const capture = NON_BUBBLING.has(type);
|
|
109
112
|
rootEl.addEventListener(type, listener, capture);
|
|
110
113
|
return () => {
|
|
111
114
|
rootEl.removeEventListener(type, listener, capture);
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
|
-
function delegateCapture(rootEl, type, selector, handler) {
|
|
117
|
+
function delegateCapture(rootEl, type, selector, handler, options) {
|
|
115
118
|
assertValidSelector(selector, "delegateCapture");
|
|
116
119
|
warnIfInsideEffect("delegateCapture");
|
|
117
|
-
const listener = (
|
|
118
|
-
const target = event.target;
|
|
119
|
-
if (!(target instanceof Element)) return;
|
|
120
|
-
if (target.matches(selector) && rootEl.contains(target)) {
|
|
121
|
-
handler(event, target);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
120
|
+
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
|
|
124
121
|
rootEl.addEventListener(type, listener, true);
|
|
125
122
|
return () => {
|
|
126
123
|
rootEl.removeEventListener(type, listener, true);
|
|
@@ -130,8 +127,8 @@ function delegateCapture(rootEl, type, selector, handler) {
|
|
|
130
127
|
// src/dev-each-warn.ts
|
|
131
128
|
var warnedIds = /* @__PURE__ */ new Set();
|
|
132
129
|
function isOptedIn() {
|
|
130
|
+
if (!isDevMode()) return false;
|
|
133
131
|
const proc = globalThis.process;
|
|
134
|
-
if (proc?.env?.NODE_ENV === "production") return false;
|
|
135
132
|
return proc?.env?.KERF_DEV_WARN_EACH_IN_MORPH_SKIP === "1";
|
|
136
133
|
}
|
|
137
134
|
function hasMorphSkipAncestor(el, root) {
|
|
@@ -153,8 +150,8 @@ function maybeWarnEachInMorphSkip(id, liveParent, rootEl) {
|
|
|
153
150
|
}
|
|
154
151
|
var warnedDupIds = /* @__PURE__ */ new Set();
|
|
155
152
|
function isOptedInDupKeys() {
|
|
153
|
+
if (!isDevMode()) return false;
|
|
156
154
|
const proc = globalThis.process;
|
|
157
|
-
if (proc?.env?.NODE_ENV === "production") return false;
|
|
158
155
|
return proc?.env?.KERF_DEV_WARN_DUPLICATE_EACH_KEYS === "1";
|
|
159
156
|
}
|
|
160
157
|
function maybeWarnDuplicateCacheKeys(id, segItems) {
|
|
@@ -173,6 +170,28 @@ function maybeWarnDuplicateCacheKeys(id, segItems) {
|
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
|
|
173
|
+
// src/list-render-state.ts
|
|
174
|
+
function deriveListRenderState(bindingCount) {
|
|
175
|
+
if (bindingCount === void 0) return "unbound";
|
|
176
|
+
return bindingCount === 0 ? "empty" : "bound";
|
|
177
|
+
}
|
|
178
|
+
function decideListPath(state, patches, snapshotLength, previousBindingCount) {
|
|
179
|
+
if (state === "unbound") return { path: "snapshot", reason: "first-render" };
|
|
180
|
+
if (state === "empty") return { path: "snapshot", reason: "empty-binding" };
|
|
181
|
+
if (patches.length === 0) return { path: "snapshot", reason: "no-patches" };
|
|
182
|
+
let netDelta = 0;
|
|
183
|
+
for (const p of patches) {
|
|
184
|
+
if (p.type === "insert") netDelta += 1;
|
|
185
|
+
else if (p.type === "remove") netDelta -= 1;
|
|
186
|
+
else if (p.type === "replace") return { path: "snapshot", reason: "replace" };
|
|
187
|
+
}
|
|
188
|
+
const count = previousBindingCount ?? 0;
|
|
189
|
+
if (count + netDelta !== snapshotLength) {
|
|
190
|
+
return { path: "snapshot", reason: "count-drift" };
|
|
191
|
+
}
|
|
192
|
+
return { path: "granular" };
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
// src/each.ts
|
|
177
196
|
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
|
|
178
197
|
function isArraySignal(value) {
|
|
@@ -204,18 +223,13 @@ function eachGranular(sig, render, cacheKey) {
|
|
|
204
223
|
const previousBindingCount = ctx.bindingCounts.get(id);
|
|
205
224
|
const patches = sig._consumePatches();
|
|
206
225
|
const snapshot = sig.value;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
else if (p.type === "replace") {
|
|
215
|
-
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (previousBindingCount + netDelta !== snapshot.length) {
|
|
226
|
+
const decision = decideListPath(
|
|
227
|
+
deriveListRenderState(previousBindingCount),
|
|
228
|
+
patches,
|
|
229
|
+
snapshot.length,
|
|
230
|
+
previousBindingCount
|
|
231
|
+
);
|
|
232
|
+
if (decision.path === "snapshot") {
|
|
219
233
|
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
220
234
|
}
|
|
221
235
|
if (cacheKey !== void 0) {
|
|
@@ -322,6 +336,11 @@ function getNodeKey(node) {
|
|
|
322
336
|
}
|
|
323
337
|
var EMPTY_OWNED = /* @__PURE__ */ new Set();
|
|
324
338
|
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
|
|
339
|
+
if (liveRoot == null) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
'morph: liveRoot is null/undefined \u2014 pass the live element, e.g. morph(document.getElementById("app")!, template). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say Element.'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
325
344
|
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
|
|
326
345
|
morphChildren(liveRoot, templateEl, ownedItems);
|
|
327
346
|
}
|
|
@@ -415,7 +434,11 @@ function morphElement(fromEl, toEl, ownedItems) {
|
|
|
415
434
|
}
|
|
416
435
|
morphAttributes(fromEl, toEl);
|
|
417
436
|
if (fromEl.dataset.morphSkipChildren !== void 0) return;
|
|
437
|
+
const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent;
|
|
418
438
|
morphChildren(fromEl, toEl, ownedItems);
|
|
439
|
+
if (syncTextareaValue) {
|
|
440
|
+
fromEl.value = toEl.textContent;
|
|
441
|
+
}
|
|
419
442
|
}
|
|
420
443
|
function isUserAgentOwnedAttr(tagName, name) {
|
|
421
444
|
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
|
|
@@ -433,6 +456,7 @@ function morphAttributes(fromEl, toEl) {
|
|
|
433
456
|
}
|
|
434
457
|
} else if (fromEl.getAttribute(name) !== value) {
|
|
435
458
|
fromEl.setAttribute(name, value);
|
|
459
|
+
syncFormProp(fromEl, name, value, true);
|
|
436
460
|
}
|
|
437
461
|
}
|
|
438
462
|
const fromAttrs = fromEl.attributes;
|
|
@@ -445,6 +469,7 @@ function morphAttributes(fromEl, toEl) {
|
|
|
445
469
|
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
|
|
446
470
|
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
|
|
447
471
|
fromEl.removeAttribute(name);
|
|
472
|
+
syncFormProp(fromEl, name, "", false);
|
|
448
473
|
}
|
|
449
474
|
}
|
|
450
475
|
}
|
|
@@ -468,13 +493,37 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
468
493
|
}
|
|
469
494
|
}
|
|
470
495
|
|
|
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
|
+
|
|
471
520
|
// src/dev-listener-warn.ts
|
|
472
521
|
var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
|
|
473
522
|
var patched = false;
|
|
474
523
|
var warned = false;
|
|
475
|
-
function
|
|
524
|
+
function isOptedIn3() {
|
|
525
|
+
if (!isDevMode()) return false;
|
|
476
526
|
const proc = globalThis.process;
|
|
477
|
-
if (proc?.env?.NODE_ENV === "production") return false;
|
|
478
527
|
return proc?.env?.KERF_DEV_WARN_REBUILT_LISTENERS === "1";
|
|
479
528
|
}
|
|
480
529
|
function findAddEventListenerProto() {
|
|
@@ -516,7 +565,7 @@ function emitWarning() {
|
|
|
516
565
|
);
|
|
517
566
|
}
|
|
518
567
|
function installListenerRebuildWarn(rootEl) {
|
|
519
|
-
if (!
|
|
568
|
+
if (!isOptedIn3()) return null;
|
|
520
569
|
patchAddEventListenerOnce();
|
|
521
570
|
const observer = new MutationObserver((mutations) => {
|
|
522
571
|
if (warned) return;
|
|
@@ -535,6 +584,47 @@ function installListenerRebuildWarn(rootEl) {
|
|
|
535
584
|
return observer;
|
|
536
585
|
}
|
|
537
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
|
+
|
|
538
628
|
// src/list-binding.ts
|
|
539
629
|
function endAnchor(binding) {
|
|
540
630
|
if (binding.items.length > 0) {
|
|
@@ -551,8 +641,8 @@ var SQUOTE = 39;
|
|
|
551
641
|
var AMP = 38;
|
|
552
642
|
var EQ = 61;
|
|
553
643
|
var SLASH = 47;
|
|
554
|
-
var
|
|
555
|
-
var
|
|
644
|
+
var TEXT_NODE3 = 3;
|
|
645
|
+
var ELEMENT_NODE3 = 1;
|
|
556
646
|
function isWhitespace(cc) {
|
|
557
647
|
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
|
|
558
648
|
}
|
|
@@ -655,13 +745,13 @@ function nthTextNodeDescendant(root, n) {
|
|
|
655
745
|
function walk(node) {
|
|
656
746
|
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
657
747
|
if (result !== null) return;
|
|
658
|
-
if (c.nodeType ===
|
|
748
|
+
if (c.nodeType === TEXT_NODE3) {
|
|
659
749
|
if (count === n) {
|
|
660
750
|
result = c;
|
|
661
751
|
return;
|
|
662
752
|
}
|
|
663
753
|
count++;
|
|
664
|
-
} else if (c.nodeType ===
|
|
754
|
+
} else if (c.nodeType === ELEMENT_NODE3) {
|
|
665
755
|
walk(c);
|
|
666
756
|
}
|
|
667
757
|
}
|
|
@@ -760,6 +850,20 @@ function parseRowTemplate(html) {
|
|
|
760
850
|
tpl.innerHTML = html;
|
|
761
851
|
return { tpl, count: tpl.content.children.length };
|
|
762
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
|
+
}
|
|
763
867
|
function rowContractError(index, html) {
|
|
764
868
|
const { count } = parseRowTemplate(html);
|
|
765
869
|
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
@@ -767,17 +871,13 @@ function rowContractError(index, html) {
|
|
|
767
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))}`
|
|
768
872
|
);
|
|
769
873
|
}
|
|
770
|
-
function
|
|
771
|
-
const proc = globalThis.process;
|
|
772
|
-
return proc?.env?.NODE_ENV !== "production";
|
|
773
|
-
}
|
|
774
|
-
function maybeWarnMissingRowKey(rowEl, rowIndex, rowHtml, binding) {
|
|
874
|
+
function maybeWarnMissingRowKey(rowEl, rowHtml, binding) {
|
|
775
875
|
if (!isDevMode()) return;
|
|
776
876
|
if (binding.warnedMissingKey === true) return;
|
|
777
877
|
binding.warnedMissingKey = true;
|
|
778
878
|
if (rowEl.id !== "" || rowEl.hasAttribute("data-key")) return;
|
|
779
879
|
console.warn(
|
|
780
|
-
`kerf each():
|
|
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))}`
|
|
781
881
|
);
|
|
782
882
|
}
|
|
783
883
|
|
|
@@ -843,12 +943,12 @@ function reconcileGranular(binding, patches) {
|
|
|
843
943
|
}
|
|
844
944
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
845
945
|
if (items.length > 0) {
|
|
846
|
-
maybeWarnMissingRowKey(items[0].node,
|
|
946
|
+
maybeWarnMissingRowKey(items[0].node, items[0].html, binding);
|
|
847
947
|
}
|
|
848
948
|
}
|
|
849
949
|
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
850
950
|
const { html } = patch;
|
|
851
|
-
const newNode = parseSingleRow(html);
|
|
951
|
+
const newNode = parseSingleRow(html, patch.index);
|
|
852
952
|
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
|
|
853
953
|
liveParent.insertBefore(newNode, anchor);
|
|
854
954
|
items.splice(patch.index, 0, {
|
|
@@ -867,12 +967,19 @@ function wireRowIfBound(node, bindings) {
|
|
|
867
967
|
function applySingleUpdate(liveParent, items, patch) {
|
|
868
968
|
const { html } = patch;
|
|
869
969
|
const oldEntry = items[patch.index];
|
|
870
|
-
if (html === oldEntry.html)
|
|
970
|
+
if (html === oldEntry.html) {
|
|
971
|
+
items[patch.index] = reuseBound(patch, html, oldEntry);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
871
974
|
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
|
|
872
975
|
items[patch.index] = reuseBound(patch, html, oldEntry);
|
|
873
976
|
return;
|
|
874
977
|
}
|
|
875
|
-
const newNode = parseSingleRow(html);
|
|
978
|
+
const newNode = parseSingleRow(html, patch.index);
|
|
979
|
+
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
|
|
980
|
+
}
|
|
981
|
+
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
|
|
982
|
+
const oldEntry = items[patch.index];
|
|
876
983
|
if (oldEntry.node.tagName === newNode.tagName) {
|
|
877
984
|
_morphElement(oldEntry.node, newNode);
|
|
878
985
|
items[patch.index] = reuseBound(patch, html, oldEntry);
|
|
@@ -890,13 +997,19 @@ function applySingleUpdate(liveParent, items, patch) {
|
|
|
890
997
|
}
|
|
891
998
|
}
|
|
892
999
|
function reuseBound(patch, html, oldEntry) {
|
|
1000
|
+
const kept = carryOrRewireRowBindings(
|
|
1001
|
+
oldEntry.node,
|
|
1002
|
+
oldEntry.bindings,
|
|
1003
|
+
oldEntry.bindingDisposers,
|
|
1004
|
+
patch.bindings
|
|
1005
|
+
);
|
|
893
1006
|
return {
|
|
894
1007
|
ref: patch.item,
|
|
895
1008
|
cacheKey: void 0,
|
|
896
1009
|
html,
|
|
897
1010
|
node: oldEntry.node,
|
|
898
|
-
bindings:
|
|
899
|
-
bindingDisposers:
|
|
1011
|
+
bindings: kept.bindings,
|
|
1012
|
+
bindingDisposers: kept.bindingDisposers
|
|
900
1013
|
};
|
|
901
1014
|
}
|
|
902
1015
|
function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
@@ -904,7 +1017,10 @@ function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
|
904
1017
|
for (let k = start; k < end; k++) {
|
|
905
1018
|
const p = patches[k];
|
|
906
1019
|
const oldEntry = items[p.index];
|
|
907
|
-
if (p.html === oldEntry.html)
|
|
1020
|
+
if (p.html === oldEntry.html) {
|
|
1021
|
+
items[p.index] = reuseBound(p, p.html, oldEntry);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
908
1024
|
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
|
|
909
1025
|
items[p.index] = reuseBound(p, p.html, oldEntry);
|
|
910
1026
|
continue;
|
|
@@ -916,50 +1032,24 @@ function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
|
916
1032
|
if (count !== morphChanges.length) {
|
|
917
1033
|
throw findOffendingChange(patches, morphChanges);
|
|
918
1034
|
}
|
|
919
|
-
const newNodes =
|
|
920
|
-
let child = tpl.content.firstElementChild;
|
|
921
|
-
for (let k = 0; k < newNodes.length; k++) {
|
|
922
|
-
newNodes[k] = child;
|
|
923
|
-
child = child.nextElementSibling;
|
|
924
|
-
}
|
|
1035
|
+
const newNodes = collectTemplateChildren(tpl, morphChanges.length);
|
|
925
1036
|
for (let k = 0; k < morphChanges.length; k++) {
|
|
926
1037
|
const c = morphChanges[k];
|
|
927
1038
|
const p = patches[c.patchIdx];
|
|
928
|
-
|
|
929
|
-
if (oldEntry.node.tagName === newNodes[k].tagName) {
|
|
930
|
-
_morphElement(oldEntry.node, newNodes[k]);
|
|
931
|
-
items[p.index] = reuseBound(p, c.html, oldEntry);
|
|
932
|
-
} else {
|
|
933
|
-
disposeRowBindings(oldEntry.bindingDisposers);
|
|
934
|
-
liveParent.replaceChild(newNodes[k], oldEntry.node);
|
|
935
|
-
items[p.index] = {
|
|
936
|
-
ref: p.item,
|
|
937
|
-
cacheKey: void 0,
|
|
938
|
-
html: c.html,
|
|
939
|
-
node: newNodes[k],
|
|
940
|
-
bindings: p.bindings,
|
|
941
|
-
bindingDisposers: wireRowIfBound(newNodes[k], p.bindings)
|
|
942
|
-
};
|
|
943
|
-
}
|
|
1039
|
+
applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]);
|
|
944
1040
|
}
|
|
945
1041
|
}
|
|
946
1042
|
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
947
1043
|
const startIdx = patches[start].index;
|
|
948
1044
|
const htmls = new Array(end - start);
|
|
949
1045
|
for (let k = start; k < end; k++) {
|
|
950
|
-
|
|
951
|
-
htmls[k - start] = p.html;
|
|
1046
|
+
htmls[k - start] = patches[k].html;
|
|
952
1047
|
}
|
|
953
1048
|
const { tpl, count } = parseRowTemplate(htmls.join(""));
|
|
954
1049
|
if (count !== htmls.length) {
|
|
955
1050
|
throw findOffendingInsert(patches, start, htmls);
|
|
956
1051
|
}
|
|
957
|
-
const newNodes =
|
|
958
|
-
let child = tpl.content.firstElementChild;
|
|
959
|
-
for (let k = 0; k < newNodes.length; k++) {
|
|
960
|
-
newNodes[k] = child;
|
|
961
|
-
child = child.nextElementSibling;
|
|
962
|
-
}
|
|
1052
|
+
const newNodes = collectTemplateChildren(tpl, end - start);
|
|
963
1053
|
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
|
|
964
1054
|
liveParent.insertBefore(tpl.content, anchor);
|
|
965
1055
|
const newEntries = new Array(end - start);
|
|
@@ -977,21 +1067,10 @@ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
|
977
1067
|
}
|
|
978
1068
|
items.splice(startIdx, 0, ...newEntries);
|
|
979
1069
|
}
|
|
980
|
-
function parseSingleRow(html) {
|
|
981
|
-
const { tpl, count } = parseRowTemplate(html);
|
|
982
|
-
if (count !== 1) {
|
|
983
|
-
const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
|
|
984
|
-
throw new Error(
|
|
985
|
-
`each() granular reconcile: row render ${reason}. Each item's render must return exactly one element. Got HTML: ${JSON.stringify(truncateRowHtml(html))}`
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
return tpl.content.firstElementChild;
|
|
989
|
-
}
|
|
990
1070
|
function findOffendingInsert(patches, start, htmls) {
|
|
991
1071
|
for (let i = 0; i < htmls.length; i++) {
|
|
992
1072
|
if (parseRowTemplate(htmls[i]).count !== 1) {
|
|
993
|
-
|
|
994
|
-
return rowContractError(p.index, htmls[i]);
|
|
1073
|
+
return rowContractError(patches[start + i].index, htmls[i]);
|
|
995
1074
|
}
|
|
996
1075
|
}
|
|
997
1076
|
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
|
|
@@ -999,8 +1078,7 @@ function findOffendingInsert(patches, start, htmls) {
|
|
|
999
1078
|
function findOffendingChange(patches, changes) {
|
|
1000
1079
|
for (const c of changes) {
|
|
1001
1080
|
if (parseRowTemplate(c.html).count !== 1) {
|
|
1002
|
-
|
|
1003
|
-
return rowContractError(p.index, c.html);
|
|
1081
|
+
return rowContractError(patches[c.patchIdx].index, c.html);
|
|
1004
1082
|
}
|
|
1005
1083
|
}
|
|
1006
1084
|
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
|
|
@@ -1023,40 +1101,45 @@ function tryInPlaceContentUpdate(binding, listSeg) {
|
|
|
1023
1101
|
}
|
|
1024
1102
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1025
1103
|
binding.items = newRecord;
|
|
1026
|
-
maybeWarnMissingRowKey(newRecord[0].node,
|
|
1104
|
+
maybeWarnMissingRowKey(newRecord[0].node, newRecord[0].html, binding);
|
|
1027
1105
|
return true;
|
|
1028
1106
|
}
|
|
1029
1107
|
function updateRowInPlace(liveParent, old, ni, index) {
|
|
1030
1108
|
if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) {
|
|
1109
|
+
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
|
|
1031
1110
|
return {
|
|
1032
1111
|
ref: ni.ref,
|
|
1033
1112
|
cacheKey: ni.cacheKey,
|
|
1034
1113
|
html: ni.html,
|
|
1035
1114
|
node: old.node,
|
|
1036
|
-
bindings:
|
|
1037
|
-
bindingDisposers:
|
|
1115
|
+
bindings: kept.bindings,
|
|
1116
|
+
bindingDisposers: kept.bindingDisposers
|
|
1038
1117
|
};
|
|
1039
1118
|
}
|
|
1040
|
-
const newNode =
|
|
1119
|
+
const newNode = parseSingleRow(ni.html, index);
|
|
1041
1120
|
if (old.node.tagName === newNode.tagName) {
|
|
1042
1121
|
_morphElement(old.node, newNode);
|
|
1122
|
+
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
|
|
1043
1123
|
return {
|
|
1044
1124
|
ref: ni.ref,
|
|
1045
1125
|
cacheKey: ni.cacheKey,
|
|
1046
1126
|
html: ni.html,
|
|
1047
1127
|
node: old.node,
|
|
1048
|
-
bindings:
|
|
1049
|
-
bindingDisposers:
|
|
1128
|
+
bindings: kept.bindings,
|
|
1129
|
+
bindingDisposers: kept.bindingDisposers
|
|
1050
1130
|
};
|
|
1051
1131
|
}
|
|
1052
1132
|
disposeRowBindings(old.bindingDisposers);
|
|
1053
1133
|
liveParent.replaceChild(newNode, old.node);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1134
|
+
const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings);
|
|
1135
|
+
return {
|
|
1136
|
+
ref: ni.ref,
|
|
1137
|
+
cacheKey: ni.cacheKey,
|
|
1138
|
+
html: ni.html,
|
|
1139
|
+
node: newNode,
|
|
1140
|
+
bindings: fresh.bindings,
|
|
1141
|
+
bindingDisposers: fresh.bindingDisposers
|
|
1142
|
+
};
|
|
1060
1143
|
}
|
|
1061
1144
|
|
|
1062
1145
|
// src/list-reconcile-snapshot.ts
|
|
@@ -1072,7 +1155,7 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
1072
1155
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
1073
1156
|
binding.items = newRecord;
|
|
1074
1157
|
if (newRecord.length > 0) {
|
|
1075
|
-
maybeWarnMissingRowKey(newRecord[0].node,
|
|
1158
|
+
maybeWarnMissingRowKey(newRecord[0].node, newRecord[0].html, binding);
|
|
1076
1159
|
}
|
|
1077
1160
|
}
|
|
1078
1161
|
function classifyItems(oldItems, listSeg) {
|
|
@@ -1098,6 +1181,8 @@ function classifyItems(oldItems, listSeg) {
|
|
|
1098
1181
|
removedItems.push(oi[0]);
|
|
1099
1182
|
}
|
|
1100
1183
|
newRecord[i] = {
|
|
1184
|
+
// `node` placeholder is filled by `buildFreshNodes`; its parse-count
|
|
1185
|
+
// check guarantees every fresh index gets a real element before use.
|
|
1101
1186
|
ref: ni.ref,
|
|
1102
1187
|
cacheKey: ni.cacheKey,
|
|
1103
1188
|
html: ni.html,
|
|
@@ -1192,37 +1277,39 @@ function reconcileList(binding, listSeg) {
|
|
|
1192
1277
|
}
|
|
1193
1278
|
|
|
1194
1279
|
// src/mount.ts
|
|
1195
|
-
var LIST_MARKER_PREFIX = "kf-list:";
|
|
1196
1280
|
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
|
|
1281
|
+
var NESTED_MOUNT_MSG = "mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts.";
|
|
1282
|
+
function isMounted(el) {
|
|
1283
|
+
return el[MOUNTED_MARKER] === true;
|
|
1284
|
+
}
|
|
1285
|
+
function setMounted(el, on) {
|
|
1286
|
+
if (on) {
|
|
1287
|
+
el[MOUNTED_MARKER] = true;
|
|
1288
|
+
} else {
|
|
1289
|
+
delete el[MOUNTED_MARKER];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1197
1292
|
function describeEl(el) {
|
|
1198
1293
|
const tag = el.tagName.toLowerCase();
|
|
1199
1294
|
const id = el.id ? `#${el.id}` : "";
|
|
1200
1295
|
return `<${tag}${id}>`;
|
|
1201
1296
|
}
|
|
1202
1297
|
function assertNotInsideMountedTree(rootEl) {
|
|
1203
|
-
if (rootEl
|
|
1298
|
+
if (isMounted(rootEl)) {
|
|
1204
1299
|
throw new Error(
|
|
1205
1300
|
`mount: ${describeEl(rootEl)} is already mounted. Call the disposer returned by the first mount() before mounting again. kerf supports one mount per element \u2014 compose with plain functions that return JSX instead of nesting mounts.`
|
|
1206
1301
|
);
|
|
1207
1302
|
}
|
|
1208
1303
|
let ancestor = rootEl.parentElement;
|
|
1209
1304
|
while (ancestor !== null) {
|
|
1210
|
-
if (ancestor
|
|
1211
|
-
throw new Error(
|
|
1212
|
-
"mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts."
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1305
|
+
if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG);
|
|
1215
1306
|
ancestor = ancestor.parentElement;
|
|
1216
1307
|
}
|
|
1217
1308
|
const stack = [];
|
|
1218
1309
|
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
|
|
1219
1310
|
while (stack.length > 0) {
|
|
1220
1311
|
const cur = stack.pop();
|
|
1221
|
-
if (cur
|
|
1222
|
-
throw new Error(
|
|
1223
|
-
"mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts."
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1312
|
+
if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG);
|
|
1226
1313
|
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
|
|
1227
1314
|
}
|
|
1228
1315
|
}
|
|
@@ -1237,7 +1324,7 @@ function mount(rootEl, render) {
|
|
|
1237
1324
|
if (owner.defaultView === null) document.adoptNode(rootEl);
|
|
1238
1325
|
}
|
|
1239
1326
|
assertNotInsideMountedTree(rootEl);
|
|
1240
|
-
rootEl
|
|
1327
|
+
setMounted(rootEl, true);
|
|
1241
1328
|
const listenerWarnObserver = installListenerRebuildWarn(rootEl);
|
|
1242
1329
|
const bindings = /* @__PURE__ */ new Map();
|
|
1243
1330
|
const renderCtx = {
|
|
@@ -1247,8 +1334,10 @@ function mount(rootEl, render) {
|
|
|
1247
1334
|
};
|
|
1248
1335
|
const bindingCtx = newBindingContext();
|
|
1249
1336
|
let bindingDisposers = [];
|
|
1337
|
+
let prevWiredBindings = [];
|
|
1250
1338
|
let isFirst = true;
|
|
1251
1339
|
let prevStaticHtml = "";
|
|
1340
|
+
const valueOnlyWarnCtx = { warned: false };
|
|
1252
1341
|
const disposeEffect = effect(() => {
|
|
1253
1342
|
renderCtx.counter = 0;
|
|
1254
1343
|
bindingCtx.counter = 0;
|
|
@@ -1267,16 +1356,32 @@ function mount(rootEl, render) {
|
|
|
1267
1356
|
runFirstRender(rootEl, segment, bindings);
|
|
1268
1357
|
prevStaticHtml = flattenWithoutListItems(segment);
|
|
1269
1358
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1359
|
+
if (isOptedIn2()) prevWiredBindings = bindingCtx.list;
|
|
1270
1360
|
isFirst = false;
|
|
1271
1361
|
} else {
|
|
1272
|
-
const nextStaticHtml = runSubsequentRender(
|
|
1362
|
+
const nextStaticHtml = runSubsequentRender(
|
|
1363
|
+
rootEl,
|
|
1364
|
+
segment,
|
|
1365
|
+
bindings,
|
|
1366
|
+
renderCtx,
|
|
1367
|
+
prevStaticHtml,
|
|
1368
|
+
valueOnlyWarnCtx
|
|
1369
|
+
);
|
|
1273
1370
|
if (nextStaticHtml !== prevStaticHtml) {
|
|
1274
1371
|
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
|
|
1372
|
+
if (isOptedIn2()) prevWiredBindings = bindingCtx.list;
|
|
1373
|
+
} else {
|
|
1374
|
+
maybeWarnStaleBinding(prevWiredBindings, bindingCtx.list);
|
|
1275
1375
|
}
|
|
1276
1376
|
prevStaticHtml = nextStaticHtml;
|
|
1277
1377
|
}
|
|
1278
1378
|
for (const listSeg of collectLists(segment).values()) {
|
|
1279
1379
|
const binding = bindings.get(listSeg.id);
|
|
1380
|
+
if (binding === void 0) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
"mount: an each() list appeared in the render output but its marker never reached the live DOM. The most common cause is an each() introduced inside a data-morph-skip subtree on a re-render \u2014 the morph leaves that subtree untouched, so the list can never bind. Move the each() outside the skipped subtree, or remove data-morph-skip from its ancestor."
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1280
1385
|
reconcileList(binding, listSeg);
|
|
1281
1386
|
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
1282
1387
|
}
|
|
@@ -1289,18 +1394,19 @@ function mount(rootEl, render) {
|
|
|
1289
1394
|
for (const item of b.items) disposeRowBindings(item.bindingDisposers);
|
|
1290
1395
|
}
|
|
1291
1396
|
listenerWarnObserver?.disconnect();
|
|
1292
|
-
|
|
1397
|
+
setMounted(rootEl, false);
|
|
1293
1398
|
};
|
|
1294
1399
|
}
|
|
1295
1400
|
function runFirstRender(rootEl, segment, bindings) {
|
|
1296
1401
|
rootEl.innerHTML = flatten(segment, true);
|
|
1297
1402
|
bindListsFromMarkers(rootEl, segment, bindings, true);
|
|
1298
1403
|
}
|
|
1299
|
-
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml) {
|
|
1404
|
+
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
|
|
1300
1405
|
const currentStaticHtml = flattenWithoutListItems(segment);
|
|
1301
1406
|
if (currentStaticHtml === prevStaticHtml) {
|
|
1302
1407
|
return prevStaticHtml;
|
|
1303
1408
|
}
|
|
1409
|
+
maybeWarnValueOnlyRerender(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
|
|
1304
1410
|
cleanupOrphanBindings(segment, bindings, renderCtx);
|
|
1305
1411
|
const template = rootEl.cloneNode(false);
|
|
1306
1412
|
template.innerHTML = currentStaticHtml;
|
|
@@ -1345,7 +1451,7 @@ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
|
1345
1451
|
}
|
|
1346
1452
|
const binding = { liveParent, items, marker };
|
|
1347
1453
|
if (items.length > 0) {
|
|
1348
|
-
maybeWarnMissingRowKey(items[0].node,
|
|
1454
|
+
maybeWarnMissingRowKey(items[0].node, items[0].html, binding);
|
|
1349
1455
|
}
|
|
1350
1456
|
maybeWarnEachInMorphSkip(id, liveParent, rootEl);
|
|
1351
1457
|
bindings.set(id, binding);
|