kerfjs 0.5.1 → 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 +50 -5
- package/README.md +17 -1
- package/dist/array-signal.js +2 -2
- package/dist/array-signal.js.map +1 -1
- package/dist/{chunk-KFOSUCCC.js → chunk-4VT4YZOO.js} +13 -4
- 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 +97 -17
- package/dist/index.js +387 -43
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +52 -2
- package/dist/jsx-runtime.js +1 -1
- package/dist/testing.js +2 -2
- package/package.json +4 -2
- 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-KFOSUCCC.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,22 +149,22 @@ 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
|
}
|
|
164
164
|
return listSafeHtml(id, segItems);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// src/
|
|
167
|
+
// src/morph.ts
|
|
168
168
|
var ID_KEY_PREFIX = "id:";
|
|
169
169
|
var DATA_KEY_PREFIX = "data-key:";
|
|
170
170
|
var ELEMENT_NODE = 1;
|
|
@@ -179,8 +179,21 @@ function getNodeKey(node) {
|
|
|
179
179
|
}
|
|
180
180
|
return void 0;
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
var EMPTY_OWNED = /* @__PURE__ */ new Set();
|
|
183
|
+
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
|
|
184
|
+
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
|
|
185
|
+
morphChildren(liveRoot, templateEl, ownedItems);
|
|
186
|
+
}
|
|
187
|
+
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
|
|
188
|
+
morphElement(fromEl, toEl, ownedItems);
|
|
189
|
+
}
|
|
190
|
+
function isElementNode(t) {
|
|
191
|
+
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
|
|
192
|
+
}
|
|
193
|
+
function parseTemplate(liveRoot, template) {
|
|
194
|
+
const el = liveRoot.cloneNode(false);
|
|
195
|
+
el.innerHTML = String(template);
|
|
196
|
+
return el;
|
|
184
197
|
}
|
|
185
198
|
function skipOwned(node, ownedItems) {
|
|
186
199
|
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
|
|
@@ -188,7 +201,7 @@ function skipOwned(node, ownedItems) {
|
|
|
188
201
|
}
|
|
189
202
|
return node;
|
|
190
203
|
}
|
|
191
|
-
function
|
|
204
|
+
function morphChildren(fromParent, toParent, ownedItems) {
|
|
192
205
|
const keyed = /* @__PURE__ */ new Map();
|
|
193
206
|
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
|
|
194
207
|
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
|
|
@@ -224,7 +237,12 @@ function diffChildren(fromParent, toParent, ownedItems) {
|
|
|
224
237
|
}
|
|
225
238
|
while (fromChild !== null) {
|
|
226
239
|
const next = fromChild.nextSibling;
|
|
227
|
-
if (fromChild.nodeType
|
|
240
|
+
if (fromChild.nodeType === ELEMENT_NODE) {
|
|
241
|
+
const el = fromChild;
|
|
242
|
+
if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) {
|
|
243
|
+
fromParent.removeChild(fromChild);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
228
246
|
fromParent.removeChild(fromChild);
|
|
229
247
|
}
|
|
230
248
|
fromChild = next;
|
|
@@ -255,7 +273,8 @@ function morphElement(fromEl, toEl, ownedItems) {
|
|
|
255
273
|
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
|
|
256
274
|
}
|
|
257
275
|
morphAttributes(fromEl, toEl);
|
|
258
|
-
|
|
276
|
+
if (fromEl.dataset.morphSkipChildren !== void 0) return;
|
|
277
|
+
morphChildren(fromEl, toEl, ownedItems);
|
|
259
278
|
}
|
|
260
279
|
function isUserAgentOwnedAttr(tagName, name) {
|
|
261
280
|
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
|
|
@@ -308,6 +327,73 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
308
327
|
}
|
|
309
328
|
}
|
|
310
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
|
+
|
|
311
397
|
// src/list-binding.ts
|
|
312
398
|
function endAnchor(binding) {
|
|
313
399
|
if (binding.items.length > 0) {
|
|
@@ -316,6 +402,184 @@ function endAnchor(binding) {
|
|
|
316
402
|
return binding.marker.nextElementSibling;
|
|
317
403
|
}
|
|
318
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
|
+
|
|
319
583
|
// src/list-reconcile-focus.ts
|
|
320
584
|
function captureFocus(liveParent) {
|
|
321
585
|
const active = document.activeElement;
|
|
@@ -362,6 +626,19 @@ function rowContractError(index, html) {
|
|
|
362
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))}`
|
|
363
627
|
);
|
|
364
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
|
+
}
|
|
365
642
|
|
|
366
643
|
// src/list-reconcile-granular.ts
|
|
367
644
|
function reconcileGranular(binding, patches) {
|
|
@@ -423,6 +700,9 @@ function reconcileGranular(binding, patches) {
|
|
|
423
700
|
}
|
|
424
701
|
}
|
|
425
702
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
703
|
+
if (items.length > 0) {
|
|
704
|
+
maybeWarnMissingRowKey(items[0].node, 0, items[0].html, binding);
|
|
705
|
+
}
|
|
426
706
|
}
|
|
427
707
|
function applySingleInsert(liveParent, items, patch, tailAnchor) {
|
|
428
708
|
const { html } = patch;
|
|
@@ -440,35 +720,53 @@ function applySingleUpdate(liveParent, items, patch) {
|
|
|
440
720
|
const { html } = patch;
|
|
441
721
|
const oldEntry = items[patch.index];
|
|
442
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
|
+
}
|
|
443
727
|
const newNode = parseSingleRow(html);
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
446
735
|
}
|
|
447
736
|
function applyBulkUpdate(liveParent, items, patches, start, end) {
|
|
448
|
-
const
|
|
737
|
+
const morphChanges = [];
|
|
449
738
|
for (let k = start; k < end; k++) {
|
|
450
739
|
const p = patches[k];
|
|
451
|
-
|
|
452
|
-
|
|
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;
|
|
453
745
|
}
|
|
746
|
+
morphChanges.push({ patchIdx: k, html: p.html });
|
|
454
747
|
}
|
|
455
|
-
if (
|
|
456
|
-
const { tpl, count } = parseRowTemplate(
|
|
457
|
-
if (count !==
|
|
458
|
-
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);
|
|
459
752
|
}
|
|
460
|
-
const newNodes = new Array(
|
|
753
|
+
const newNodes = new Array(morphChanges.length);
|
|
461
754
|
let child = tpl.content.firstElementChild;
|
|
462
755
|
for (let k = 0; k < newNodes.length; k++) {
|
|
463
756
|
newNodes[k] = child;
|
|
464
757
|
child = child.nextElementSibling;
|
|
465
758
|
}
|
|
466
|
-
for (let k = 0; k <
|
|
467
|
-
const c =
|
|
759
|
+
for (let k = 0; k < morphChanges.length; k++) {
|
|
760
|
+
const c = morphChanges[k];
|
|
468
761
|
const p = patches[c.patchIdx];
|
|
469
762
|
const oldEntry = items[p.index];
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
}
|
|
472
770
|
}
|
|
473
771
|
}
|
|
474
772
|
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
|
|
@@ -537,6 +835,9 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
537
835
|
const { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
|
|
538
836
|
if (replacedNodes.length === 0 && freshIndices.length === 0 && isInOrder(prevIdx)) {
|
|
539
837
|
binding.items = newRecord;
|
|
838
|
+
if (newRecord.length > 0) {
|
|
839
|
+
maybeWarnMissingRowKey(newRecord[0].node, 0, newRecord[0].html, binding);
|
|
840
|
+
}
|
|
540
841
|
return;
|
|
541
842
|
}
|
|
542
843
|
const tailAnchor = endAnchor(binding);
|
|
@@ -546,6 +847,9 @@ function reconcileSnapshot(binding, listSeg) {
|
|
|
546
847
|
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
|
|
547
848
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
548
849
|
binding.items = newRecord;
|
|
850
|
+
if (newRecord.length > 0) {
|
|
851
|
+
maybeWarnMissingRowKey(newRecord[0].node, 0, newRecord[0].html, binding);
|
|
852
|
+
}
|
|
549
853
|
}
|
|
550
854
|
function classifyItems(oldItems, listSeg) {
|
|
551
855
|
const oldByRef = /* @__PURE__ */ new Map();
|
|
@@ -665,12 +969,43 @@ function reconcileList(binding, listSeg) {
|
|
|
665
969
|
|
|
666
970
|
// src/mount.ts
|
|
667
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
|
+
}
|
|
668
1000
|
function mount(rootEl, render) {
|
|
669
1001
|
if (rootEl == null) {
|
|
670
1002
|
throw new Error(
|
|
671
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.'
|
|
672
1004
|
);
|
|
673
1005
|
}
|
|
1006
|
+
assertNotInsideMountedTree(rootEl);
|
|
1007
|
+
rootEl[MOUNTED_MARKER] = true;
|
|
1008
|
+
const listenerWarnObserver = installListenerRebuildWarn(rootEl);
|
|
674
1009
|
const bindings = /* @__PURE__ */ new Map();
|
|
675
1010
|
const renderCtx = {
|
|
676
1011
|
counter: 0,
|
|
@@ -679,7 +1014,7 @@ function mount(rootEl, render) {
|
|
|
679
1014
|
};
|
|
680
1015
|
let isFirst = true;
|
|
681
1016
|
let prevStaticHtml = "";
|
|
682
|
-
|
|
1017
|
+
const disposeEffect = effect(() => {
|
|
683
1018
|
renderCtx.counter = 0;
|
|
684
1019
|
_setRenderContext(renderCtx);
|
|
685
1020
|
let result;
|
|
@@ -702,6 +1037,11 @@ function mount(rootEl, render) {
|
|
|
702
1037
|
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
|
|
703
1038
|
}
|
|
704
1039
|
});
|
|
1040
|
+
return () => {
|
|
1041
|
+
disposeEffect();
|
|
1042
|
+
listenerWarnObserver?.disconnect();
|
|
1043
|
+
delete rootEl[MOUNTED_MARKER];
|
|
1044
|
+
};
|
|
705
1045
|
}
|
|
706
1046
|
function runFirstRender(rootEl, segment, bindings) {
|
|
707
1047
|
rootEl.innerHTML = flatten(segment, true);
|
|
@@ -715,7 +1055,7 @@ function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtm
|
|
|
715
1055
|
cleanupOrphanBindings(segment, bindings, renderCtx);
|
|
716
1056
|
const template = rootEl.cloneNode(false);
|
|
717
1057
|
template.innerHTML = currentStaticHtml;
|
|
718
|
-
|
|
1058
|
+
morph(rootEl, template, collectOwnedItems(bindings));
|
|
719
1059
|
bindListsFromMarkers(rootEl, segment, bindings, false);
|
|
720
1060
|
return currentStaticHtml;
|
|
721
1061
|
}
|
|
@@ -748,7 +1088,11 @@ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
|
|
|
748
1088
|
next = next.nextElementSibling;
|
|
749
1089
|
}
|
|
750
1090
|
}
|
|
751
|
-
|
|
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);
|
|
752
1096
|
}
|
|
753
1097
|
}
|
|
754
1098
|
function validateInlinedRowMatch(expectedHtml, index, boundEl) {
|
|
@@ -855,6 +1199,6 @@ function toElement(jsx) {
|
|
|
855
1199
|
return child;
|
|
856
1200
|
}
|
|
857
1201
|
|
|
858
|
-
export { delegate, delegateCapture, each, mount, toElement };
|
|
1202
|
+
export { delegate, delegateCapture, each, morph, mount, toElement };
|
|
859
1203
|
//# sourceMappingURL=index.js.map
|
|
860
1204
|
//# sourceMappingURL=index.js.map
|