kerfjs 0.6.0 → 0.7.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 +28 -0
- package/README.md +1 -0
- package/dist/array-signal.js +1 -1
- package/dist/{chunk-LADH5GVQ.js → chunk-4VT4YZOO.js} +11 -2
- package/dist/chunk-4VT4YZOO.js.map +1 -0
- package/dist/chunk-UU2YJEJY.js +41 -0
- package/dist/chunk-UU2YJEJY.js.map +1 -0
- package/dist/{chunk-GQGJFCWL.js → chunk-Z7ZHXD2R.js} +14 -4
- package/dist/chunk-Z7ZHXD2R.js.map +1 -0
- package/dist/index.d.ts +34 -13
- package/dist/index.js +363 -35
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +46 -0
- package/dist/jsx-runtime.js +1 -1
- package/dist/testing.js +2 -2
- package/package.json +2 -1
- package/dist/chunk-FN2ID4QO.js +0 -3
- package/dist/chunk-FN2ID4QO.js.map +0 -1
- package/dist/chunk-GQGJFCWL.js.map +0 -1
- package/dist/chunk-LADH5GVQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-
|
|
2
|
-
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-
|
|
3
|
-
export { defineStore, resetAllStores } from './chunk-
|
|
4
|
-
import { effect } from './chunk-
|
|
5
|
-
export { batch, computed, effect, signal } from './chunk-
|
|
1
|
+
import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-4VT4YZOO.js';
|
|
2
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-4VT4YZOO.js';
|
|
3
|
+
export { defineStore, resetAllStores } from './chunk-Z7ZHXD2R.js';
|
|
4
|
+
import { effect } from './chunk-UU2YJEJY.js';
|
|
5
|
+
export { batch, computed, effect, signal } from './chunk-UU2YJEJY.js';
|
|
6
6
|
|
|
7
7
|
// src/delegate.ts
|
|
8
8
|
var NON_BUBBLING = /* @__PURE__ */ new Set([
|
|
@@ -63,41 +63,41 @@ var context = null;
|
|
|
63
63
|
function _setRenderContext(c) {
|
|
64
64
|
context = c;
|
|
65
65
|
}
|
|
66
|
-
function each(items, render,
|
|
66
|
+
function each(items, render, cacheKey) {
|
|
67
67
|
if (isArraySignal(items) && context !== null) {
|
|
68
|
-
return eachGranular(items, render,
|
|
68
|
+
return eachGranular(items, render, cacheKey);
|
|
69
69
|
}
|
|
70
70
|
const snapshotItems = isArraySignal(items) ? items.value : items;
|
|
71
|
-
return eachSnapshot(snapshotItems, render,
|
|
71
|
+
return eachSnapshot(snapshotItems, render, cacheKey);
|
|
72
72
|
}
|
|
73
|
-
function eachSnapshot(items, render,
|
|
73
|
+
function eachSnapshot(items, render, cacheKey) {
|
|
74
74
|
let id;
|
|
75
75
|
if (context !== null) {
|
|
76
76
|
id = String(context.counter++);
|
|
77
77
|
} else {
|
|
78
78
|
id = "orphan";
|
|
79
79
|
}
|
|
80
|
-
return eachSnapshotById(items, render,
|
|
80
|
+
return eachSnapshotById(items, render, cacheKey, id);
|
|
81
81
|
}
|
|
82
|
-
function eachGranular(sig, render,
|
|
82
|
+
function eachGranular(sig, render, cacheKey) {
|
|
83
83
|
const ctx = context;
|
|
84
84
|
const id = String(ctx.counter++);
|
|
85
85
|
const previousBindingCount = ctx.bindingCounts.get(id);
|
|
86
86
|
const patches = sig._consumePatches();
|
|
87
87
|
const snapshot = sig.value;
|
|
88
88
|
if (previousBindingCount === void 0 || patches.length === 0) {
|
|
89
|
-
return eachSnapshotById(snapshot, render,
|
|
89
|
+
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
90
90
|
}
|
|
91
91
|
let netDelta = 0;
|
|
92
92
|
for (const p of patches) {
|
|
93
93
|
if (p.type === "insert") netDelta += 1;
|
|
94
94
|
else if (p.type === "remove") netDelta -= 1;
|
|
95
95
|
else if (p.type === "replace") {
|
|
96
|
-
return eachSnapshotById(snapshot, render,
|
|
96
|
+
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
if (previousBindingCount + netDelta !== snapshot.length) {
|
|
100
|
-
return eachSnapshotById(snapshot, render,
|
|
100
|
+
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
101
101
|
}
|
|
102
102
|
const renderFnInternal = (item, index) => {
|
|
103
103
|
const out = render(item, index);
|
|
@@ -120,11 +120,11 @@ function eachGranular(sig, render, key) {
|
|
|
120
120
|
}
|
|
121
121
|
} catch {
|
|
122
122
|
ctx.bindingCounts.delete(id);
|
|
123
|
-
return eachSnapshotById(snapshot, render,
|
|
123
|
+
return eachSnapshotById(snapshot, render, cacheKey, id);
|
|
124
124
|
}
|
|
125
125
|
return granularListSafeHtml(id, [], internalPatches);
|
|
126
126
|
}
|
|
127
|
-
function eachSnapshotById(items, render,
|
|
127
|
+
function eachSnapshotById(items, render, cacheKey, id) {
|
|
128
128
|
let cache = null;
|
|
129
129
|
if (context !== null) {
|
|
130
130
|
let c = context.caches.get(id);
|
|
@@ -149,15 +149,15 @@ function eachSnapshotById(items, render, key, id) {
|
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
151
|
seen.add(item);
|
|
152
|
-
const k =
|
|
152
|
+
const k = cacheKey ? cacheKey(item, i) : void 0;
|
|
153
153
|
let html;
|
|
154
154
|
const cached = cache !== null ? cache.get(item) : void 0;
|
|
155
|
-
if (cached !== void 0 && cached.
|
|
155
|
+
if (cached !== void 0 && cached.cacheKey === k) {
|
|
156
156
|
html = cached.html;
|
|
157
157
|
} else {
|
|
158
158
|
const out = render(item, i);
|
|
159
159
|
html = isSafeHtml(out) ? out.toString() : out;
|
|
160
|
-
if (cache !== null) cache.set(item, {
|
|
160
|
+
if (cache !== null) cache.set(item, { cacheKey: k, html });
|
|
161
161
|
}
|
|
162
162
|
segItems[i] = { ref: item, cacheKey: k, html };
|
|
163
163
|
}
|
|
@@ -184,6 +184,9 @@ function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
|
|
|
184
184
|
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
|
|
185
185
|
morphChildren(liveRoot, templateEl, ownedItems);
|
|
186
186
|
}
|
|
187
|
+
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
|
|
188
|
+
morphElement(fromEl, toEl, ownedItems);
|
|
189
|
+
}
|
|
187
190
|
function isElementNode(t) {
|
|
188
191
|
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
|
|
189
192
|
}
|
|
@@ -324,6 +327,73 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
// src/dev-listener-warn.ts
|
|
331
|
+
var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
|
|
332
|
+
var patched = false;
|
|
333
|
+
var warned = false;
|
|
334
|
+
function isOptedIn() {
|
|
335
|
+
const proc = globalThis.process;
|
|
336
|
+
if (proc?.env?.NODE_ENV === "production") return false;
|
|
337
|
+
return proc?.env?.KERF_DEV_WARN_REBUILT_LISTENERS === "1";
|
|
338
|
+
}
|
|
339
|
+
function findAddEventListenerProto() {
|
|
340
|
+
const probe = document.createElement("div");
|
|
341
|
+
let proto = Object.getPrototypeOf(probe);
|
|
342
|
+
while (!Object.prototype.hasOwnProperty.call(proto, "addEventListener")) {
|
|
343
|
+
proto = Object.getPrototypeOf(proto);
|
|
344
|
+
}
|
|
345
|
+
return proto;
|
|
346
|
+
}
|
|
347
|
+
function patchAddEventListenerOnce() {
|
|
348
|
+
if (patched) return;
|
|
349
|
+
patched = true;
|
|
350
|
+
const proto = findAddEventListenerProto();
|
|
351
|
+
const orig = proto.addEventListener;
|
|
352
|
+
proto.addEventListener = function(type, listener, options) {
|
|
353
|
+
if (this instanceof Element) {
|
|
354
|
+
this[LISTENER_MARKER] = true;
|
|
355
|
+
}
|
|
356
|
+
return orig.call(this, type, listener, options);
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function hasMarkedListener(el) {
|
|
360
|
+
if (el[LISTENER_MARKER] === true) return true;
|
|
361
|
+
const stack = [];
|
|
362
|
+
for (let i = 0; i < el.children.length; i++) stack.push(el.children[i]);
|
|
363
|
+
while (stack.length > 0) {
|
|
364
|
+
const cur = stack.pop();
|
|
365
|
+
if (cur[LISTENER_MARKER] === true) return true;
|
|
366
|
+
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
function emitWarning() {
|
|
371
|
+
if (warned) return;
|
|
372
|
+
warned = true;
|
|
373
|
+
console.warn(
|
|
374
|
+
"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."
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
function installListenerRebuildWarn(rootEl) {
|
|
378
|
+
if (!isOptedIn()) return null;
|
|
379
|
+
patchAddEventListenerOnce();
|
|
380
|
+
const observer = new MutationObserver((mutations) => {
|
|
381
|
+
if (warned) return;
|
|
382
|
+
for (const m of mutations) {
|
|
383
|
+
for (let i = 0; i < m.removedNodes.length; i++) {
|
|
384
|
+
const removed = m.removedNodes[i];
|
|
385
|
+
if (!(removed instanceof Element)) continue;
|
|
386
|
+
if (hasMarkedListener(removed)) {
|
|
387
|
+
emitWarning();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
observer.observe(rootEl, { childList: true, subtree: true });
|
|
394
|
+
return observer;
|
|
395
|
+
}
|
|
396
|
+
|
|
327
397
|
// src/list-binding.ts
|
|
328
398
|
function endAnchor(binding) {
|
|
329
399
|
if (binding.items.length > 0) {
|
|
@@ -332,6 +402,184 @@ function endAnchor(binding) {
|
|
|
332
402
|
return binding.marker.nextElementSibling;
|
|
333
403
|
}
|
|
334
404
|
|
|
405
|
+
// src/list-reconcile-fast-paths.ts
|
|
406
|
+
var LT = 60;
|
|
407
|
+
var GT = 62;
|
|
408
|
+
var DQUOTE = 34;
|
|
409
|
+
var SQUOTE = 39;
|
|
410
|
+
var AMP = 38;
|
|
411
|
+
var EQ = 61;
|
|
412
|
+
var SLASH = 47;
|
|
413
|
+
var TEXT_NODE2 = 3;
|
|
414
|
+
var ELEMENT_NODE2 = 1;
|
|
415
|
+
function isWhitespace(cc) {
|
|
416
|
+
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
|
|
417
|
+
}
|
|
418
|
+
function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
|
|
419
|
+
const oldGt = oldHtml.indexOf(">");
|
|
420
|
+
const newGt = newHtml.indexOf(">");
|
|
421
|
+
if (oldGt === -1 || newGt === -1) return false;
|
|
422
|
+
if (oldHtml.length - oldGt !== newHtml.length - newGt) return false;
|
|
423
|
+
if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false;
|
|
424
|
+
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
|
|
425
|
+
const oldTag = parseOpeningTag(oldHtml, oldGt);
|
|
426
|
+
const newTag = parseOpeningTag(newHtml, newGt);
|
|
427
|
+
if (oldTag === null || newTag === null) return false;
|
|
428
|
+
if (oldTag.tagName !== newTag.tagName) return false;
|
|
429
|
+
for (const name of oldTag.attrs.keys()) {
|
|
430
|
+
if (name.indexOf(":") !== -1) return false;
|
|
431
|
+
}
|
|
432
|
+
for (const name of newTag.attrs.keys()) {
|
|
433
|
+
if (name.indexOf(":") !== -1) return false;
|
|
434
|
+
}
|
|
435
|
+
const liveTagUpper = liveNode.tagName;
|
|
436
|
+
for (const [name, rawValue] of newTag.attrs) {
|
|
437
|
+
const oldValue = oldTag.attrs.get(name);
|
|
438
|
+
if (oldValue === rawValue) continue;
|
|
439
|
+
liveNode.setAttribute(name, unescapeAttrValue(rawValue));
|
|
440
|
+
}
|
|
441
|
+
for (const name of oldTag.attrs.keys()) {
|
|
442
|
+
if (newTag.attrs.has(name)) continue;
|
|
443
|
+
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
|
|
444
|
+
liveNode.removeAttribute(name);
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
|
|
449
|
+
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
|
|
450
|
+
let p = 0;
|
|
451
|
+
const minLen = Math.min(oldHtml.length, newHtml.length);
|
|
452
|
+
while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++;
|
|
453
|
+
let s = 0;
|
|
454
|
+
const maxS = minLen - p;
|
|
455
|
+
while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) {
|
|
456
|
+
s++;
|
|
457
|
+
}
|
|
458
|
+
const oldWinEnd = oldHtml.length - s;
|
|
459
|
+
const newWinEnd = newHtml.length - s;
|
|
460
|
+
if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false;
|
|
461
|
+
if (!isPureTextWindow(newHtml, p, newWinEnd)) return false;
|
|
462
|
+
if (p === 0) return false;
|
|
463
|
+
const boundaryCc = oldHtml.charCodeAt(p - 1);
|
|
464
|
+
if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false;
|
|
465
|
+
const textStart = lastIndexOfChar(oldHtml, GT, p - 1);
|
|
466
|
+
if (textStart === -1) return false;
|
|
467
|
+
const textEnd = oldHtml.indexOf("<", p);
|
|
468
|
+
if (textEnd === -1) return false;
|
|
469
|
+
if (textEnd < oldWinEnd) return false;
|
|
470
|
+
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
|
|
471
|
+
const oldText = oldHtml.slice(textStart + 1, textEnd);
|
|
472
|
+
const newText = newHtml.slice(textStart + 1, newTextEnd);
|
|
473
|
+
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
|
|
474
|
+
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
|
|
475
|
+
if (targetNode === null) return false;
|
|
476
|
+
if (targetNode.nodeValue !== oldText) return false;
|
|
477
|
+
targetNode.nodeValue = newText;
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
function containsDataMorphSkip(html) {
|
|
481
|
+
return html.indexOf("data-morph-skip") !== -1;
|
|
482
|
+
}
|
|
483
|
+
function isPureTextWindow(html, start, end) {
|
|
484
|
+
for (let i = start; i < end; i++) {
|
|
485
|
+
const cc = html.charCodeAt(i);
|
|
486
|
+
if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
function lastIndexOfChar(html, target, beforeInclusive) {
|
|
491
|
+
for (let i = beforeInclusive; i >= 0; i--) {
|
|
492
|
+
if (html.charCodeAt(i) === target) return i;
|
|
493
|
+
}
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
496
|
+
function countTextNodesBefore(html, beforePos) {
|
|
497
|
+
let count = 0;
|
|
498
|
+
let i = 0;
|
|
499
|
+
while (i < beforePos) {
|
|
500
|
+
if (html.charCodeAt(i) === LT) {
|
|
501
|
+
while (i < beforePos && html.charCodeAt(i) !== GT) i++;
|
|
502
|
+
i++;
|
|
503
|
+
} else {
|
|
504
|
+
const start = i;
|
|
505
|
+
while (i < beforePos && html.charCodeAt(i) !== LT) i++;
|
|
506
|
+
if (i > start) count++;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return count;
|
|
510
|
+
}
|
|
511
|
+
function nthTextNodeDescendant(root, n) {
|
|
512
|
+
let count = 0;
|
|
513
|
+
let result = null;
|
|
514
|
+
function walk(node) {
|
|
515
|
+
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
516
|
+
if (result !== null) return;
|
|
517
|
+
if (c.nodeType === TEXT_NODE2) {
|
|
518
|
+
if (count === n) {
|
|
519
|
+
result = c;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
count++;
|
|
523
|
+
} else if (c.nodeType === ELEMENT_NODE2) {
|
|
524
|
+
walk(c);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
walk(root);
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
function parseOpeningTag(html, gtPos) {
|
|
532
|
+
if (html.charCodeAt(0) !== LT) return null;
|
|
533
|
+
let i = 1;
|
|
534
|
+
let end = gtPos;
|
|
535
|
+
if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1;
|
|
536
|
+
const nameStart = i;
|
|
537
|
+
while (i < end) {
|
|
538
|
+
const cc = html.charCodeAt(i);
|
|
539
|
+
if (isWhitespace(cc)) break;
|
|
540
|
+
i++;
|
|
541
|
+
}
|
|
542
|
+
const tagName = html.slice(nameStart, i);
|
|
543
|
+
if (tagName.length === 0) return null;
|
|
544
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
545
|
+
while (i < end) {
|
|
546
|
+
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
|
|
547
|
+
if (i >= end) break;
|
|
548
|
+
const aNameStart = i;
|
|
549
|
+
while (i < end) {
|
|
550
|
+
const cc = html.charCodeAt(i);
|
|
551
|
+
if (cc === EQ || isWhitespace(cc)) break;
|
|
552
|
+
i++;
|
|
553
|
+
}
|
|
554
|
+
const aName = html.slice(aNameStart, i);
|
|
555
|
+
if (aName.length === 0) return null;
|
|
556
|
+
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
|
|
557
|
+
if (i < end && html.charCodeAt(i) === EQ) {
|
|
558
|
+
i++;
|
|
559
|
+
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
|
|
560
|
+
if (i >= end) return null;
|
|
561
|
+
const q = html.charCodeAt(i);
|
|
562
|
+
if (q !== DQUOTE && q !== SQUOTE) return null;
|
|
563
|
+
i++;
|
|
564
|
+
const vStart = i;
|
|
565
|
+
while (i < end && html.charCodeAt(i) !== q) i++;
|
|
566
|
+
if (i >= end) return null;
|
|
567
|
+
attrs.set(aName, html.slice(vStart, i));
|
|
568
|
+
i++;
|
|
569
|
+
} else {
|
|
570
|
+
attrs.set(aName, "");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return { tagName, attrs };
|
|
574
|
+
}
|
|
575
|
+
function unescapeAttrValue(s) {
|
|
576
|
+
if (s.indexOf("&") === -1) return s;
|
|
577
|
+
return s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
578
|
+
}
|
|
579
|
+
function isUserAgentOwnedAttr2(tagNameUpper, name) {
|
|
580
|
+
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
|
|
581
|
+
}
|
|
582
|
+
|
|
335
583
|
// src/list-reconcile-focus.ts
|
|
336
584
|
function captureFocus(liveParent) {
|
|
337
585
|
const active = document.activeElement;
|
|
@@ -378,6 +626,19 @@ function rowContractError(index, html) {
|
|
|
378
626
|
`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))}`
|
|
379
627
|
);
|
|
380
628
|
}
|
|
629
|
+
function isDevMode() {
|
|
630
|
+
const proc = globalThis.process;
|
|
631
|
+
return proc?.env?.NODE_ENV !== "production";
|
|
632
|
+
}
|
|
633
|
+
function maybeWarnMissingRowKey(rowEl, rowIndex, rowHtml, binding) {
|
|
634
|
+
if (!isDevMode()) return;
|
|
635
|
+
if (binding.warnedMissingKey === true) return;
|
|
636
|
+
binding.warnedMissingKey = true;
|
|
637
|
+
if (rowEl.id !== "" || rowEl.hasAttribute("data-key")) return;
|
|
638
|
+
console.warn(
|
|
639
|
+
`kerf each(): row at index ${rowIndex} 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))}`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
381
642
|
|
|
382
643
|
// src/list-reconcile-granular.ts
|
|
383
644
|
function reconcileGranular(binding, patches) {
|
|
@@ -439,6 +700,9 @@ function reconcileGranular(binding, patches) {
|
|
|
439
700
|
}
|
|
440
701
|
}
|
|
441
702
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
703
|
+
if (items.length > 0) {
|
|
704
|
+
maybeWarnMissingRowKey(items[0].node, 0, items[0].html, binding);
|
|
705
|
+
}
|
|
442
706
|
}
|
|
443
707
|
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
444
708
|
const { html } = patch;
|
|
@@ -456,35 +720,53 @@ function applySingleUpdate(liveParent, items, patch) {
|
|
|
456
720
|
const { html } = patch;
|
|
457
721
|
const oldEntry = items[patch.index];
|
|
458
722
|
if (html === oldEntry.html) return;
|
|
723
|
+
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
|
|
724
|
+
items[patch.index] = { ref: patch.item, cacheKey: void 0, html, node: oldEntry.node };
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
459
727
|
const newNode = parseSingleRow(html);
|
|
460
|
-
|
|
461
|
-
|
|
728
|
+
if (oldEntry.node.tagName === newNode.tagName) {
|
|
729
|
+
_morphElement(oldEntry.node, newNode);
|
|
730
|
+
items[patch.index] = { ref: patch.item, cacheKey: void 0, html, node: oldEntry.node };
|
|
731
|
+
} else {
|
|
732
|
+
liveParent.replaceChild(newNode, oldEntry.node);
|
|
733
|
+
items[patch.index] = { ref: patch.item, cacheKey: void 0, html, node: newNode };
|
|
734
|
+
}
|
|
462
735
|
}
|
|
463
736
|
function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
464
|
-
const
|
|
737
|
+
const morphChanges = [];
|
|
465
738
|
for (let k = start; k < end; k++) {
|
|
466
739
|
const p = patches[k];
|
|
467
|
-
|
|
468
|
-
|
|
740
|
+
const oldEntry = items[p.index];
|
|
741
|
+
if (p.html === oldEntry.html) continue;
|
|
742
|
+
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
|
|
743
|
+
items[p.index] = { ref: p.item, cacheKey: void 0, html: p.html, node: oldEntry.node };
|
|
744
|
+
continue;
|
|
469
745
|
}
|
|
746
|
+
morphChanges.push({ patchIdx: k, html: p.html });
|
|
470
747
|
}
|
|
471
|
-
if (
|
|
472
|
-
const { tpl, count } = parseRowTemplate(
|
|
473
|
-
if (count !==
|
|
474
|
-
throw findOffendingChange(patches,
|
|
748
|
+
if (morphChanges.length === 0) return;
|
|
749
|
+
const { tpl, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""));
|
|
750
|
+
if (count !== morphChanges.length) {
|
|
751
|
+
throw findOffendingChange(patches, morphChanges);
|
|
475
752
|
}
|
|
476
|
-
const newNodes = new Array(
|
|
753
|
+
const newNodes = new Array(morphChanges.length);
|
|
477
754
|
let child = tpl.content.firstElementChild;
|
|
478
755
|
for (let k = 0; k < newNodes.length; k++) {
|
|
479
756
|
newNodes[k] = child;
|
|
480
757
|
child = child.nextElementSibling;
|
|
481
758
|
}
|
|
482
|
-
for (let k = 0; k <
|
|
483
|
-
const c =
|
|
759
|
+
for (let k = 0; k < morphChanges.length; k++) {
|
|
760
|
+
const c = morphChanges[k];
|
|
484
761
|
const p = patches[c.patchIdx];
|
|
485
762
|
const oldEntry = items[p.index];
|
|
486
|
-
|
|
487
|
-
|
|
763
|
+
if (oldEntry.node.tagName === newNodes[k].tagName) {
|
|
764
|
+
_morphElement(oldEntry.node, newNodes[k]);
|
|
765
|
+
items[p.index] = { ref: p.item, cacheKey: void 0, html: c.html, node: oldEntry.node };
|
|
766
|
+
} else {
|
|
767
|
+
liveParent.replaceChild(newNodes[k], oldEntry.node);
|
|
768
|
+
items[p.index] = { ref: p.item, cacheKey: void 0, html: c.html, node: newNodes[k] };
|
|
769
|
+
}
|
|
488
770
|
}
|
|
489
771
|
}
|
|
490
772
|
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
@@ -553,6 +835,9 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
553
835
|
const { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
|
|
554
836
|
if (replacedNodes.length === 0 && freshIndices.length === 0 && isInOrder(prevIdx)) {
|
|
555
837
|
binding.items = newRecord;
|
|
838
|
+
if (newRecord.length > 0) {
|
|
839
|
+
maybeWarnMissingRowKey(newRecord[0].node, 0, newRecord[0].html, binding);
|
|
840
|
+
}
|
|
556
841
|
return;
|
|
557
842
|
}
|
|
558
843
|
const tailAnchor = endAnchor(binding);
|
|
@@ -562,6 +847,9 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
562
847
|
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
|
|
563
848
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
564
849
|
binding.items = newRecord;
|
|
850
|
+
if (newRecord.length > 0) {
|
|
851
|
+
maybeWarnMissingRowKey(newRecord[0].node, 0, newRecord[0].html, binding);
|
|
852
|
+
}
|
|
565
853
|
}
|
|
566
854
|
function classifyItems(oldItems, listSeg) {
|
|
567
855
|
const oldByRef = /* @__PURE__ */ new Map();
|
|
@@ -681,12 +969,43 @@ function reconcileList(binding, listSeg) {
|
|
|
681
969
|
|
|
682
970
|
// src/mount.ts
|
|
683
971
|
var LIST_MARKER_PREFIX = "kf-list:";
|
|
972
|
+
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
|
|
973
|
+
function assertNotInsideMountedTree(rootEl) {
|
|
974
|
+
if (rootEl[MOUNTED_MARKER] === true) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
"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."
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
let ancestor = rootEl.parentElement;
|
|
980
|
+
while (ancestor !== null) {
|
|
981
|
+
if (ancestor[MOUNTED_MARKER] === true) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
"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."
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
ancestor = ancestor.parentElement;
|
|
987
|
+
}
|
|
988
|
+
const stack = [];
|
|
989
|
+
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
|
|
990
|
+
while (stack.length > 0) {
|
|
991
|
+
const cur = stack.pop();
|
|
992
|
+
if (cur[MOUNTED_MARKER] === true) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
"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."
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
684
1000
|
function mount(rootEl, render) {
|
|
685
1001
|
if (rootEl == null) {
|
|
686
1002
|
throw new Error(
|
|
687
1003
|
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
|
|
688
1004
|
);
|
|
689
1005
|
}
|
|
1006
|
+
assertNotInsideMountedTree(rootEl);
|
|
1007
|
+
rootEl[MOUNTED_MARKER] = true;
|
|
1008
|
+
const listenerWarnObserver = installListenerRebuildWarn(rootEl);
|
|
690
1009
|
const bindings = /* @__PURE__ */ new Map();
|
|
691
1010
|
const renderCtx = {
|
|
692
1011
|
counter: 0,
|
|
@@ -695,7 +1014,7 @@ function mount(rootEl, render) {
|
|
|
695
1014
|
};
|
|
696
1015
|
let isFirst = true;
|
|
697
1016
|
let prevStaticHtml = "";
|
|
698
|
-
|
|
1017
|
+
const disposeEffect = effect(() => {
|
|
699
1018
|
renderCtx.counter = 0;
|
|
700
1019
|
_setRenderContext(renderCtx);
|
|
701
1020
|
let result;
|
|
@@ -718,6 +1037,11 @@ function mount(rootEl, render) {
|
|
|
718
1037
|
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
719
1038
|
}
|
|
720
1039
|
});
|
|
1040
|
+
return () => {
|
|
1041
|
+
disposeEffect();
|
|
1042
|
+
listenerWarnObserver?.disconnect();
|
|
1043
|
+
delete rootEl[MOUNTED_MARKER];
|
|
1044
|
+
};
|
|
721
1045
|
}
|
|
722
1046
|
function runFirstRender(rootEl, segment, bindings) {
|
|
723
1047
|
rootEl.innerHTML = flatten(segment, true);
|
|
@@ -764,7 +1088,11 @@ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
|
764
1088
|
next = next.nextElementSibling;
|
|
765
1089
|
}
|
|
766
1090
|
}
|
|
767
|
-
|
|
1091
|
+
const binding = { liveParent, items, marker };
|
|
1092
|
+
if (items.length > 0) {
|
|
1093
|
+
maybeWarnMissingRowKey(items[0].node, 0, items[0].html, binding);
|
|
1094
|
+
}
|
|
1095
|
+
bindings.set(id, binding);
|
|
768
1096
|
}
|
|
769
1097
|
}
|
|
770
1098
|
function validateInlinedRowMatch(expectedHtml, index, boundEl) {
|