| import { itemVersion } from './chunk-QIP723L4.js'; | ||
| import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js'; | ||
| import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-SUPUPSBE.js'; | ||
| import { effect } from './chunk-3APBEVHF.js'; | ||
| import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js'; | ||
| import { devHooks } from './chunk-VVDJLWMP.js'; | ||
| // src/list-render-state.ts | ||
| function deriveListRenderState(bindingCount) { | ||
| if (bindingCount === void 0) return "unbound"; | ||
| return bindingCount === 0 ? "empty" : "bound"; | ||
| } | ||
| function decideListPath(state, patches, snapshotLength, previousBindingCount) { | ||
| if (state === "unbound") return { path: "snapshot", reason: "first-render" }; | ||
| if (state === "empty") return { path: "snapshot", reason: "empty-binding" }; | ||
| if (patches.length === 0) return { path: "snapshot", reason: "no-patches" }; | ||
| let netDelta = 0; | ||
| for (const p of patches) { | ||
| if (p.type === "insert") netDelta += 1; | ||
| else if (p.type === "remove") netDelta -= 1; | ||
| else if (p.type === "replace") return { path: "snapshot", reason: "replace" }; | ||
| } | ||
| const count = previousBindingCount ?? 0; | ||
| if (count + netDelta !== snapshotLength) { | ||
| return { path: "snapshot", reason: "count-drift" }; | ||
| } | ||
| return { path: "granular" }; | ||
| } | ||
| // src/each.ts | ||
| var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal"); | ||
| function isArraySignal(value) { | ||
| return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true; | ||
| } | ||
| var context = null; | ||
| var renderingRow = false; | ||
| function inRowScope(fn) { | ||
| const prev = renderingRow; | ||
| renderingRow = true; | ||
| try { | ||
| return fn(); | ||
| } finally { | ||
| renderingRow = prev; | ||
| } | ||
| } | ||
| function _setRenderContext(c) { | ||
| context = c; | ||
| } | ||
| function _resetCallOrderListState(ctx) { | ||
| const isCallOrderId = (id) => !id.startsWith("k:"); | ||
| for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) { | ||
| for (const id of Array.from(map.keys())) { | ||
| if (isCallOrderId(id)) map.delete(id); | ||
| } | ||
| } | ||
| } | ||
| function isEachOptions(v) { | ||
| return typeof v === "object" && v !== null; | ||
| } | ||
| var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/; | ||
| function assertValidKey(key) { | ||
| if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) { | ||
| throw new Error( | ||
| `each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.` | ||
| ); | ||
| } | ||
| } | ||
| function claimKey(ctx, key) { | ||
| assertValidKey(key); | ||
| if (renderingRow) { | ||
| throw new Error( | ||
| `each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.` | ||
| ); | ||
| } | ||
| if (ctx.keysThisRender.has(key)) { | ||
| throw new Error( | ||
| `each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.` | ||
| ); | ||
| } | ||
| ctx.keysThisRender.add(key); | ||
| return `k:${key}`; | ||
| } | ||
| function each(items, render, cacheKeyOrOptions) { | ||
| const useOptions = isEachOptions(cacheKeyOrOptions); | ||
| const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions; | ||
| const listKey = useOptions ? cacheKeyOrOptions.key : void 0; | ||
| if (isArraySignal(items) && context !== null) { | ||
| return eachGranular(items, render, cacheKey, listKey); | ||
| } | ||
| const snapshotItems = isArraySignal(items) ? items.value : items; | ||
| return eachSnapshot(snapshotItems, render, cacheKey, listKey); | ||
| } | ||
| function eachSnapshot(items, render, cacheKey, listKey) { | ||
| let id; | ||
| if (context !== null) { | ||
| id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++); | ||
| } else { | ||
| id = "orphan"; | ||
| } | ||
| return eachSnapshotById(items, render, cacheKey, id); | ||
| } | ||
| function assertObjectItem(item, index) { | ||
| if (typeof item !== "object" || item === null) { | ||
| throw new Error( | ||
| `each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).` | ||
| ); | ||
| } | ||
| } | ||
| function eachGranular(sig, render, cacheKey, listKey) { | ||
| const ctx = context; | ||
| const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++); | ||
| const previousBindingCount = ctx.bindingCounts.get(id); | ||
| const patches = sig._consumePatches(); | ||
| const snapshot = sig.value; | ||
| const previousSource = ctx.bindingSources.get(id); | ||
| const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig; | ||
| if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id); | ||
| const decision = sourceReused ? { path: "snapshot" } : decideListPath( | ||
| deriveListRenderState(previousBindingCount), | ||
| patches, | ||
| snapshot.length, | ||
| previousBindingCount | ||
| ); | ||
| if (decision.path === "snapshot") { | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| let staleIndexShift = false; | ||
| if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) { | ||
| const rendered = []; | ||
| for (let i = 0; i < previousBindingCount; i++) rendered.push(i); | ||
| for (const p of patches) { | ||
| if (p.type === "insert") rendered.splice(p.index, 0, p.index); | ||
| else if (p.type === "remove") rendered.splice(p.index, 1); | ||
| else if (p.type === "move") { | ||
| const [moved] = rendered.splice(p.from, 1); | ||
| rendered.splice(p.to, 0, moved); | ||
| } | ||
| } | ||
| for (let i = 0; i < rendered.length; i++) { | ||
| if (rendered[i] !== i) { | ||
| staleIndexShift = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (cacheKey !== void 0) { | ||
| const cache2 = ctx.caches.get(id); | ||
| for (let i = 0; i < snapshot.length; i++) { | ||
| const item = snapshot[i]; | ||
| const k = cacheKey(item, i); | ||
| const cached = cache2.get(item); | ||
| if (cached !== void 0 && cached.cacheKey !== k) { | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| } | ||
| } | ||
| const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => { | ||
| const out = render(item, index); | ||
| return isSafeHtml(out) ? out.toString() : out; | ||
| })); | ||
| const internalPatches = new Array(patches.length); | ||
| const cache = ctx.caches.get(id); | ||
| try { | ||
| for (let i = 0; i < patches.length; i++) { | ||
| const p = patches[i]; | ||
| if (p.type === "insert" || p.type === "update") { | ||
| assertObjectItem(p.item, p.index); | ||
| const { html, bindings } = renderRow(p.item, p.index); | ||
| internalPatches[i] = { | ||
| type: p.type, | ||
| index: p.index, | ||
| item: p.item, | ||
| html, | ||
| bindings | ||
| }; | ||
| cache?.set(p.item, { | ||
| cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0, | ||
| html, | ||
| bindings, | ||
| version: itemVersion(p.item), | ||
| index: p.index | ||
| }); | ||
| } else { | ||
| internalPatches[i] = p; | ||
| } | ||
| } | ||
| } catch { | ||
| ctx.bindingCounts.delete(id); | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| if (staleIndexShift) devHooks.staleIndex?.(id); | ||
| return granularListSafeHtml(id, [], internalPatches, sig); | ||
| } | ||
| function eachSnapshotById(items, render, cacheKey, id, source) { | ||
| let cache = null; | ||
| if (context !== null) { | ||
| let c = context.caches.get(id); | ||
| if (c === void 0) { | ||
| c = /* @__PURE__ */ new WeakMap(); | ||
| context.caches.set(id, c); | ||
| } | ||
| cache = c; | ||
| } | ||
| const segItems = new Array(items.length); | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (let i = 0; i < items.length; i++) { | ||
| const item = items[i]; | ||
| assertObjectItem(item, i); | ||
| if (seen.has(item)) { | ||
| throw new Error( | ||
| `each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).` | ||
| ); | ||
| } | ||
| seen.add(item); | ||
| const k = cacheKey ? cacheKey(item, i) : void 0; | ||
| const version = itemVersion(item); | ||
| let html; | ||
| let bindings; | ||
| const cached = cache !== null ? cache.get(item) : void 0; | ||
| if (cached !== void 0 && cached.cacheKey === k && cached.version === version) { | ||
| html = cached.html; | ||
| bindings = cached.bindings; | ||
| if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) { | ||
| devHooks.staleIndex?.(id); | ||
| } | ||
| } else { | ||
| const captured = captureRowBindings(() => inRowScope(() => { | ||
| const out = render(item, i); | ||
| return isSafeHtml(out) ? out.toString() : out; | ||
| })); | ||
| html = captured.html; | ||
| bindings = captured.bindings; | ||
| if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i }); | ||
| } | ||
| segItems[i] = { ref: item, cacheKey: k, html, bindings }; | ||
| } | ||
| if (cacheKey !== void 0) { | ||
| devHooks.duplicateCacheKeys?.(id, segItems); | ||
| } | ||
| return listSafeHtml(id, segItems, source); | ||
| } | ||
| // src/utils/moveNode.ts | ||
| function moveNode(parent, node, ref) { | ||
| const move = parent.moveBefore; | ||
| if (move !== void 0 && node.isConnected) { | ||
| move.call(parent, node, ref); | ||
| } else { | ||
| parent.insertBefore(node, ref); | ||
| } | ||
| } | ||
| // src/list-reconcile-focus.ts | ||
| function captureFocus(liveParent) { | ||
| const active = document.activeElement; | ||
| if (active === null || active === document.body) return null; | ||
| if (!liveParent.contains(active)) return null; | ||
| const el = active; | ||
| let selStart = null; | ||
| let selEnd = null; | ||
| if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") { | ||
| try { | ||
| selStart = el.selectionStart; | ||
| selEnd = el.selectionEnd; | ||
| } catch { | ||
| } | ||
| } | ||
| return { el, selStart, selEnd }; | ||
| } | ||
| function restoreFocus(snap) { | ||
| if (document.activeElement === snap.el) return; | ||
| if (!snap.el.isConnected) return; | ||
| snap.el.focus(); | ||
| if (snap.selStart !== null && snap.selEnd !== null) { | ||
| try { | ||
| snap.el.setSelectionRange(snap.selStart, snap.selEnd); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| // src/morph.ts | ||
| var ID_KEY_PREFIX = "id:"; | ||
| var DATA_KEY_PREFIX = "data-key:"; | ||
| var ELEMENT_NODE = 1; | ||
| var TEXT_NODE = 3; | ||
| var COMMENT_NODE = 8; | ||
| function getNodeKey(node) { | ||
| if (node.nodeType !== ELEMENT_NODE) return void 0; | ||
| const el = node; | ||
| if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`; | ||
| if (el.dataset !== void 0 && el.dataset.key !== void 0) { | ||
| return `${DATA_KEY_PREFIX}${el.dataset.key}`; | ||
| } | ||
| return void 0; | ||
| } | ||
| var EMPTY_OWNED = /* @__PURE__ */ new Set(); | ||
| function morph(liveRoot, template, ownedItems = EMPTY_OWNED) { | ||
| if (liveRoot == null) { | ||
| throw new Error( | ||
| '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.' | ||
| ); | ||
| } | ||
| const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template); | ||
| const focusSnap = captureFocus(liveRoot); | ||
| morphChildren(liveRoot, templateEl, ownedItems); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| } | ||
| function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) { | ||
| morphElement(fromEl, toEl, ownedItems); | ||
| } | ||
| function isElementNode(t) { | ||
| return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE; | ||
| } | ||
| function parseTemplate(liveRoot, template) { | ||
| const el = liveRoot.cloneNode(false); | ||
| el.innerHTML = String(template); | ||
| return el; | ||
| } | ||
| function protectionTag(node) { | ||
| const { dataset } = node; | ||
| return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : ""); | ||
| } | ||
| var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX]; | ||
| function isMarker(node) { | ||
| if (node.nodeType !== COMMENT_NODE) return false; | ||
| const { data } = node; | ||
| return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix)); | ||
| } | ||
| function markersPairable(a, b) { | ||
| if (!isMarker(a) && !isMarker(b)) return true; | ||
| return a.data === b.data; | ||
| } | ||
| function skipOwned(node, ownedItems) { | ||
| while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) { | ||
| node = node.nextSibling; | ||
| } | ||
| return node; | ||
| } | ||
| function isListMarker(node) { | ||
| return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX); | ||
| } | ||
| function afterListRegion(marker, ownedItems) { | ||
| let last = marker; | ||
| for (let r = marker.nextSibling; r !== null; r = r.nextSibling) { | ||
| if (isListMarker(r)) break; | ||
| if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r; | ||
| } | ||
| return last.nextSibling; | ||
| } | ||
| function morphChildren(fromParent, toParent, ownedItems) { | ||
| const keyed = /* @__PURE__ */ new Map(); | ||
| for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) { | ||
| if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue; | ||
| const k = getNodeKey(c); | ||
| if (k !== void 0) keyed.set(k, c); | ||
| } | ||
| let fromChild = skipOwned(fromParent.firstChild, ownedItems); | ||
| let toChild = toParent.firstChild; | ||
| while (toChild !== null) { | ||
| const toNext = toChild.nextSibling; | ||
| let matched = null; | ||
| const toKey = getNodeKey(toChild); | ||
| if (toKey !== void 0 && keyed.has(toKey)) { | ||
| matched = keyed.get(toKey); | ||
| keyed.delete(toKey); | ||
| if (matched !== fromChild) { | ||
| moveNode(fromParent, matched, fromChild); | ||
| } else { | ||
| fromChild = skipOwned(fromChild.nextSibling, ownedItems); | ||
| } | ||
| } | ||
| if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) { | ||
| matched = fromChild; | ||
| fromChild = skipOwned( | ||
| isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling, | ||
| ownedItems | ||
| ); | ||
| if (matched.nodeType === COMMENT_NODE && fromChild !== null) { | ||
| const owned = boundTextNodeOf(matched); | ||
| if (owned !== null && fromChild === owned) { | ||
| fromChild = skipOwned(owned.nextSibling, ownedItems); | ||
| } | ||
| } | ||
| } | ||
| if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) { | ||
| const toTag = toChild.tagName; | ||
| for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) { | ||
| if (scan.nodeType !== ELEMENT_NODE) continue; | ||
| const el = scan; | ||
| if (ownedItems.has(el)) continue; | ||
| if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue; | ||
| if (protectionTag(el) !== protectionTag(toChild)) continue; | ||
| matched = el; | ||
| moveNode(fromParent, el, fromChild); | ||
| break; | ||
| } | ||
| } | ||
| if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) { | ||
| const wantData = toChild.data; | ||
| for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) { | ||
| if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue; | ||
| const regionEnd = afterListRegion(scan, ownedItems); | ||
| const run = []; | ||
| for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) { | ||
| run.push(r); | ||
| } | ||
| const focusSnap = captureFocus(fromParent); | ||
| for (const node of run) moveNode(fromParent, node, fromChild); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| matched = scan; | ||
| break; | ||
| } | ||
| } | ||
| if (matched !== null) { | ||
| morphNode(matched, toChild, ownedItems); | ||
| } else { | ||
| const cloned = toChild.cloneNode(true); | ||
| fromParent.insertBefore(cloned, fromChild); | ||
| } | ||
| toChild = toNext; | ||
| } | ||
| while (fromChild !== null) { | ||
| const next = fromChild.nextSibling; | ||
| if (fromChild.nodeType === ELEMENT_NODE) { | ||
| const el = fromChild; | ||
| if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) { | ||
| fromParent.removeChild(fromChild); | ||
| } | ||
| } else { | ||
| fromParent.removeChild(fromChild); | ||
| } | ||
| fromChild = next; | ||
| } | ||
| } | ||
| function morphNode(fromNode, toNode, ownedItems) { | ||
| if (fromNode.nodeType === ELEMENT_NODE) { | ||
| morphElement(fromNode, toNode, ownedItems); | ||
| return; | ||
| } | ||
| if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) { | ||
| const fromText = fromNode; | ||
| const toText = toNode; | ||
| if (fromText.data !== toText.data) fromText.data = toText.data; | ||
| } | ||
| } | ||
| function morphElement(fromEl, toEl, ownedItems) { | ||
| if (fromEl.tagName !== toEl.tagName) { | ||
| const replacement = toEl.cloneNode(true); | ||
| fromEl.parentNode?.replaceChild(replacement, fromEl); | ||
| return; | ||
| } | ||
| if (fromEl.dataset.morphSkip !== void 0) return; | ||
| if (fromEl.isEqualNode(toEl)) return; | ||
| if (fromEl === document.activeElement) { | ||
| const ce = fromEl.getAttribute("contenteditable"); | ||
| if (ce !== null && ce.toLowerCase() !== "false") return; | ||
| if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl); | ||
| } | ||
| morphAttributes(fromEl, toEl); | ||
| if (fromEl.dataset.morphSkipChildren !== void 0) return; | ||
| const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent; | ||
| morphChildren(fromEl, toEl, ownedItems); | ||
| if (syncTextareaValue) { | ||
| fromEl.value = toEl.textContent; | ||
| } | ||
| } | ||
| function isUserAgentOwnedAttr(tagName, name) { | ||
| return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG"); | ||
| } | ||
| function morphAttributes(fromEl, toEl) { | ||
| const toAttrs = toEl.attributes; | ||
| for (let i = 0; i < toAttrs.length; i++) { | ||
| const attr = toAttrs[i]; | ||
| const ns = attr.namespaceURI; | ||
| const name = attr.localName; | ||
| const value = attr.value; | ||
| if (ns !== null) { | ||
| if (fromEl.getAttributeNS(ns, name) !== value) { | ||
| fromEl.setAttributeNS(ns, attr.name, value); | ||
| } | ||
| } else if (fromEl.getAttribute(name) !== value) { | ||
| fromEl.setAttribute(name, value); | ||
| syncFormProp(fromEl, name, value, true); | ||
| } | ||
| } | ||
| const fromAttrs = fromEl.attributes; | ||
| const fromTag = fromEl.tagName; | ||
| for (let i = fromAttrs.length - 1; i >= 0; i--) { | ||
| const attr = fromAttrs[i]; | ||
| const ns = attr.namespaceURI; | ||
| const name = attr.localName; | ||
| if (ns !== null) { | ||
| if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name); | ||
| } else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) { | ||
| fromEl.removeAttribute(name); | ||
| syncFormProp(fromEl, name, "", false); | ||
| } | ||
| } | ||
| } | ||
| function isTextInputOrTextarea(el) { | ||
| if (el.tagName === "TEXTAREA") return true; | ||
| if (el.tagName === "INPUT") { | ||
| const type = el.type; | ||
| return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === ""; | ||
| } | ||
| return false; | ||
| } | ||
| function preserveTextEntryState(fromEl, toEl) { | ||
| if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") { | ||
| const fromInput = fromEl; | ||
| const toInput = toEl; | ||
| toInput.value = fromInput.value; | ||
| try { | ||
| toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| // src/list-binding.ts | ||
| function endAnchor(binding) { | ||
| if (binding.items.length > 0) { | ||
| return binding.items[binding.items.length - 1].node.nextSibling; | ||
| } | ||
| return binding.marker.nextSibling; | ||
| } | ||
| // src/list-reconcile-fast-paths.ts | ||
| var LT = 60; | ||
| var GT = 62; | ||
| var DQUOTE = 34; | ||
| var SQUOTE = 39; | ||
| var AMP = 38; | ||
| var EQ = 61; | ||
| var SLASH = 47; | ||
| var TEXT_NODE2 = 3; | ||
| var ELEMENT_NODE2 = 1; | ||
| function isWhitespace(cc) { | ||
| return cc === 32 || cc === 9 || cc === 10 || cc === 13; | ||
| } | ||
| function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) { | ||
| const oldGt = oldHtml.indexOf(">"); | ||
| const newGt = newHtml.indexOf(">"); | ||
| if (oldGt === -1 || newGt === -1) return false; | ||
| if (oldHtml.length - oldGt !== newHtml.length - newGt) return false; | ||
| if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false; | ||
| if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false; | ||
| const oldTag = parseOpeningTag(oldHtml, oldGt); | ||
| const newTag = parseOpeningTag(newHtml, newGt); | ||
| if (oldTag === null || newTag === null) return false; | ||
| if (oldTag.tagName !== newTag.tagName) return false; | ||
| for (const name of oldTag.attrs.keys()) { | ||
| if (name.indexOf(":") !== -1) return false; | ||
| } | ||
| for (const name of newTag.attrs.keys()) { | ||
| if (name.indexOf(":") !== -1) return false; | ||
| } | ||
| const liveTagUpper = liveNode.tagName; | ||
| for (const [name, rawValue] of newTag.attrs) { | ||
| const oldValue = oldTag.attrs.get(name); | ||
| if (oldValue === rawValue) continue; | ||
| const value = unescapeAttrValue(rawValue); | ||
| liveNode.setAttribute(name, value); | ||
| syncFormProp(liveNode, name, value, true); | ||
| } | ||
| for (const name of oldTag.attrs.keys()) { | ||
| if (newTag.attrs.has(name)) continue; | ||
| if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue; | ||
| liveNode.removeAttribute(name); | ||
| syncFormProp(liveNode, name, "", false); | ||
| } | ||
| return true; | ||
| } | ||
| function tryTextContentFastPath(liveNode, oldHtml, newHtml) { | ||
| if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false; | ||
| let p = 0; | ||
| const minLen = Math.min(oldHtml.length, newHtml.length); | ||
| while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++; | ||
| let s = 0; | ||
| const maxS = minLen - p; | ||
| while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) { | ||
| s++; | ||
| } | ||
| const oldWinEnd = oldHtml.length - s; | ||
| const newWinEnd = newHtml.length - s; | ||
| if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false; | ||
| if (!isPureTextWindow(newHtml, p, newWinEnd)) return false; | ||
| if (p === 0) return false; | ||
| const boundaryCc = oldHtml.charCodeAt(p - 1); | ||
| if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false; | ||
| const textStart = lastIndexOfChar(oldHtml, GT, p - 1); | ||
| if (textStart === -1) return false; | ||
| const textEnd = oldHtml.indexOf("<", p); | ||
| if (textEnd === -1) return false; | ||
| if (textEnd < oldWinEnd) return false; | ||
| const newTextEnd = textEnd + (newHtml.length - oldHtml.length); | ||
| const oldText = oldHtml.slice(textStart + 1, textEnd); | ||
| const newText = newHtml.slice(textStart + 1, newTextEnd); | ||
| if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false; | ||
| const textIdx = countTextNodesBefore(oldHtml, textStart + 1); | ||
| const targetNode = nthTextNodeDescendant(liveNode, textIdx); | ||
| if (targetNode === null) return false; | ||
| if (targetNode.nodeValue !== oldText) return false; | ||
| targetNode.nodeValue = newText; | ||
| const host = targetNode.parentNode; | ||
| if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) { | ||
| host.value = newText; | ||
| } | ||
| return true; | ||
| } | ||
| function containsDataMorphSkip(html) { | ||
| return html.indexOf("data-morph-skip") !== -1; | ||
| } | ||
| function isPureTextWindow(html, start, end) { | ||
| for (let i = start; i < end; i++) { | ||
| const cc = html.charCodeAt(i); | ||
| if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function lastIndexOfChar(html, target, beforeInclusive) { | ||
| for (let i = beforeInclusive; i >= 0; i--) { | ||
| if (html.charCodeAt(i) === target) return i; | ||
| } | ||
| return -1; | ||
| } | ||
| function countTextNodesBefore(html, beforePos) { | ||
| let count = 0; | ||
| let i = 0; | ||
| while (i < beforePos) { | ||
| if (html.charCodeAt(i) === LT) { | ||
| while (i < beforePos && html.charCodeAt(i) !== GT) i++; | ||
| i++; | ||
| } else { | ||
| const start = i; | ||
| while (i < beforePos && html.charCodeAt(i) !== LT) i++; | ||
| if (i > start) count++; | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
| function nthTextNodeDescendant(root, n) { | ||
| let count = 0; | ||
| let result = null; | ||
| function walk(node) { | ||
| for (let c = node.firstChild; c !== null; c = c.nextSibling) { | ||
| if (result !== null) return; | ||
| if (c.nodeType === TEXT_NODE2) { | ||
| if (count === n) { | ||
| result = c; | ||
| return; | ||
| } | ||
| count++; | ||
| } else if (c.nodeType === ELEMENT_NODE2) { | ||
| walk(c); | ||
| } | ||
| } | ||
| } | ||
| walk(root); | ||
| return result; | ||
| } | ||
| function parseOpeningTag(html, gtPos) { | ||
| if (html.charCodeAt(0) !== LT) return null; | ||
| let i = 1; | ||
| let end = gtPos; | ||
| if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1; | ||
| const nameStart = i; | ||
| while (i < end) { | ||
| const cc = html.charCodeAt(i); | ||
| if (isWhitespace(cc)) break; | ||
| i++; | ||
| } | ||
| const tagName = html.slice(nameStart, i); | ||
| if (tagName.length === 0) return null; | ||
| const attrs = /* @__PURE__ */ new Map(); | ||
| while (i < end) { | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i >= end) break; | ||
| const aNameStart = i; | ||
| while (i < end) { | ||
| const cc = html.charCodeAt(i); | ||
| if (cc === EQ || isWhitespace(cc)) break; | ||
| i++; | ||
| } | ||
| const aName = html.slice(aNameStart, i); | ||
| if (aName.length === 0) return null; | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i < end && html.charCodeAt(i) === EQ) { | ||
| i++; | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i >= end) return null; | ||
| const q = html.charCodeAt(i); | ||
| if (q !== DQUOTE && q !== SQUOTE) return null; | ||
| i++; | ||
| const vStart = i; | ||
| while (i < end && html.charCodeAt(i) !== q) i++; | ||
| if (i >= end) return null; | ||
| attrs.set(aName, html.slice(vStart, i)); | ||
| i++; | ||
| } else { | ||
| attrs.set(aName, ""); | ||
| } | ||
| } | ||
| return { tagName, attrs }; | ||
| } | ||
| function unescapeAttrValue(s) { | ||
| if (s.indexOf("&") === -1) return s; | ||
| return s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"); | ||
| } | ||
| function isUserAgentOwnedAttr2(tagNameUpper, name) { | ||
| return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG"); | ||
| } | ||
| // src/list-reconcile-granular.ts | ||
| function reconcileGranular(binding, patches) { | ||
| const { liveParent } = binding; | ||
| const items = binding.items; | ||
| const focusSnap = captureFocus(liveParent); | ||
| let i = 0; | ||
| while (i < patches.length) { | ||
| const patch = patches[i]; | ||
| if (patch.type === "replace") { | ||
| i += 1; | ||
| continue; | ||
| } | ||
| if (patch.type === "update") { | ||
| let runEnd = i + 1; | ||
| while (runEnd < patches.length && patches[runEnd].type === "update") { | ||
| runEnd += 1; | ||
| } | ||
| const runLen = runEnd - i; | ||
| if (runLen === 1) { | ||
| applySingleUpdate(liveParent, items, patch); | ||
| } else { | ||
| applyBulkUpdate(liveParent, items, patches, i, runEnd); | ||
| } | ||
| i = runEnd; | ||
| continue; | ||
| } | ||
| if (patch.type === "insert") { | ||
| let runEnd = i + 1; | ||
| while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) { | ||
| runEnd += 1; | ||
| } | ||
| const runLen = runEnd - i; | ||
| if (runLen === 1) { | ||
| applySingleInsert(liveParent, items, patch, endAnchor(binding)); | ||
| } else { | ||
| applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding)); | ||
| } | ||
| i = runEnd; | ||
| continue; | ||
| } | ||
| if (patch.type === "remove") { | ||
| const entry = items[patch.index]; | ||
| disposeRowBindings(entry.bindingDisposers); | ||
| liveParent.removeChild(entry.node); | ||
| items.splice(patch.index, 1); | ||
| i += 1; | ||
| continue; | ||
| } | ||
| if (patch.type === "move") { | ||
| const moved = items[patch.from]; | ||
| let anchorIdx = patch.to; | ||
| if (patch.from < patch.to) anchorIdx += 1; | ||
| const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding); | ||
| moveNode(liveParent, moved.node, anchor); | ||
| items.splice(patch.from, 1); | ||
| items.splice(patch.to, 0, moved); | ||
| i += 1; | ||
| continue; | ||
| } | ||
| } | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| if (items.length > 0) { | ||
| devHooks.missingRowKey?.(items[0].node, items[0].html, binding); | ||
| } | ||
| } | ||
| function applySingleInsert(liveParent, items, patch, tailAnchor) { | ||
| const { html } = patch; | ||
| const newNode = parseSingleRow(html, patch.index, liveParent); | ||
| const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor; | ||
| liveParent.insertBefore(newNode, anchor); | ||
| items.splice(patch.index, 0, { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: newNode, | ||
| bindings: patch.bindings, | ||
| // KF-294: wire the inserted row's fine-grained bindings to its new node. | ||
| bindingDisposers: wireRowIfBound(newNode, patch.bindings) | ||
| }); | ||
| } | ||
| function wireRowIfBound(node, bindings) { | ||
| return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0; | ||
| } | ||
| function applySingleUpdate(liveParent, items, patch) { | ||
| const { html } = patch; | ||
| const oldEntry = items[patch.index]; | ||
| if (html === oldEntry.html) { | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| return; | ||
| } | ||
| if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) { | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| return; | ||
| } | ||
| const newNode = parseSingleRow(html, patch.index, liveParent); | ||
| applyParsedRowUpdate(liveParent, items, patch, html, newNode); | ||
| } | ||
| function applyParsedRowUpdate(liveParent, items, patch, html, newNode) { | ||
| const oldEntry = items[patch.index]; | ||
| if (oldEntry.node.tagName === newNode.tagName) { | ||
| _morphElement(oldEntry.node, newNode); | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| } else { | ||
| disposeRowBindings(oldEntry.bindingDisposers); | ||
| liveParent.replaceChild(newNode, oldEntry.node); | ||
| items[patch.index] = { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: newNode, | ||
| bindings: patch.bindings, | ||
| bindingDisposers: wireRowIfBound(newNode, patch.bindings) | ||
| }; | ||
| } | ||
| } | ||
| function reuseBound(patch, html, oldEntry) { | ||
| const kept = carryOrRewireRowBindings( | ||
| oldEntry.node, | ||
| oldEntry.bindings, | ||
| oldEntry.bindingDisposers, | ||
| patch.bindings | ||
| ); | ||
| return { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: oldEntry.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| function applyBulkUpdate(liveParent, items, patches, start, end) { | ||
| const morphChanges = []; | ||
| for (let k = start; k < end; k++) { | ||
| const p = patches[k]; | ||
| const oldEntry = items[p.index]; | ||
| if (p.html === oldEntry.html) { | ||
| items[p.index] = reuseBound(p, p.html, oldEntry); | ||
| continue; | ||
| } | ||
| if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) { | ||
| items[p.index] = reuseBound(p, p.html, oldEntry); | ||
| continue; | ||
| } | ||
| morphChanges.push({ patchIdx: k, html: p.html }); | ||
| } | ||
| if (morphChanges.length === 0) return; | ||
| const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent); | ||
| if (count !== morphChanges.length) { | ||
| throw findOffendingChange(patches, morphChanges, liveParent); | ||
| } | ||
| const newNodes = collectTemplateChildren(content, morphChanges.length); | ||
| for (let k = 0; k < morphChanges.length; k++) { | ||
| const c = morphChanges[k]; | ||
| const p = patches[c.patchIdx]; | ||
| applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]); | ||
| } | ||
| } | ||
| function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) { | ||
| const startIdx = patches[start].index; | ||
| const htmls = new Array(end - start); | ||
| for (let k = start; k < end; k++) { | ||
| htmls[k - start] = patches[k].html; | ||
| } | ||
| const { content, count } = parseRowTemplate(htmls.join(""), liveParent); | ||
| if (count !== htmls.length) { | ||
| throw findOffendingInsert(patches, start, htmls, liveParent); | ||
| } | ||
| const newNodes = collectTemplateChildren(content, end - start); | ||
| const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor; | ||
| liveParent.insertBefore(content, anchor); | ||
| const newEntries = new Array(end - start); | ||
| for (let k = 0; k < newEntries.length; k++) { | ||
| const p = patches[start + k]; | ||
| newEntries[k] = { | ||
| ref: p.item, | ||
| cacheKey: void 0, | ||
| html: htmls[k], | ||
| node: newNodes[k], | ||
| bindings: p.bindings, | ||
| bindingDisposers: wireRowIfBound(newNodes[k], p.bindings) | ||
| // KF-294 | ||
| }; | ||
| } | ||
| items.splice(startIdx, 0, ...newEntries); | ||
| } | ||
| function findOffendingInsert(patches, start, htmls, liveParent) { | ||
| for (let i = 0; i < htmls.length; i++) { | ||
| if (parseRowTemplate(htmls[i], liveParent).count !== 1) { | ||
| return rowContractError(patches[start + i].index, htmls[i], liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| function findOffendingChange(patches, changes, liveParent) { | ||
| for (const c of changes) { | ||
| if (parseRowTemplate(c.html, liveParent).count !== 1) { | ||
| return rowContractError(patches[c.patchIdx].index, c.html, liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| // src/list-reconcile-inplace.ts | ||
| function tryInPlaceContentUpdate(binding, listSeg) { | ||
| const oldItems = binding.items; | ||
| const items = listSeg.items; | ||
| const n = items.length; | ||
| if (n === 0 || n !== oldItems.length) return false; | ||
| for (let i = 0; i < n; i++) { | ||
| if (items[i].ref !== oldItems[i].ref) return false; | ||
| } | ||
| const { liveParent } = binding; | ||
| const newRecord = new Array(n); | ||
| const focusSnap = captureFocus(liveParent); | ||
| for (let i = 0; i < n; i++) { | ||
| newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i); | ||
| } | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| binding.items = newRecord; | ||
| devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding); | ||
| return true; | ||
| } | ||
| function updateRowInPlace(liveParent, old, ni, index) { | ||
| if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) { | ||
| const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: old.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| const newNode = parseSingleRow(ni.html, index, liveParent); | ||
| if (old.node.tagName === newNode.tagName) { | ||
| _morphElement(old.node, newNode); | ||
| const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: old.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| disposeRowBindings(old.bindingDisposers); | ||
| liveParent.replaceChild(newNode, old.node); | ||
| const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: newNode, | ||
| bindings: fresh.bindings, | ||
| bindingDisposers: fresh.bindingDisposers | ||
| }; | ||
| } | ||
| // src/list-reconcile-snapshot.ts | ||
| function reconcileSnapshot(binding, listSeg) { | ||
| if (tryInPlaceContentUpdate(binding, listSeg)) return; | ||
| const { liveParent } = binding; | ||
| const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg); | ||
| const tailAnchor = endAnchor(binding); | ||
| buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent); | ||
| const focusSnap = captureFocus(liveParent); | ||
| removeOldNodes(liveParent, removedItems); | ||
| applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| binding.items = newRecord; | ||
| if (newRecord.length > 0) { | ||
| devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding); | ||
| } | ||
| } | ||
| function classifyItems(oldItems, listSeg) { | ||
| const oldByRef = /* @__PURE__ */ new Map(); | ||
| for (let i = 0; i < oldItems.length; i++) { | ||
| oldByRef.set(oldItems[i].ref, [oldItems[i], i]); | ||
| } | ||
| const newRecord = new Array(listSeg.items.length); | ||
| const prevIdx = new Array(listSeg.items.length); | ||
| const removedItems = []; | ||
| const freshIndices = []; | ||
| const freshHtmls = []; | ||
| for (let i = 0; i < listSeg.items.length; i++) { | ||
| const ni = listSeg.items[i]; | ||
| const oi = oldByRef.get(ni.ref); | ||
| if (oi !== void 0) { | ||
| oldByRef.delete(ni.ref); | ||
| if (oi[0].html === ni.html) { | ||
| newRecord[i] = oi[0]; | ||
| prevIdx[i] = oi[1]; | ||
| continue; | ||
| } | ||
| removedItems.push(oi[0]); | ||
| } | ||
| newRecord[i] = { | ||
| // `node` placeholder is filled by `buildFreshNodes`; its parse-count | ||
| // check guarantees every fresh index gets a real element before use. | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: null, | ||
| bindings: ni.bindings | ||
| }; | ||
| prevIdx[i] = -1; | ||
| freshIndices.push(i); | ||
| freshHtmls.push(ni.html); | ||
| } | ||
| for (const [, orphan] of oldByRef) removedItems.push(orphan[0]); | ||
| return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls }; | ||
| } | ||
| function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) { | ||
| if (freshHtmls.length === 0) return; | ||
| const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent); | ||
| if (count !== freshHtmls.length) { | ||
| throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent); | ||
| } | ||
| let node = content.firstElementChild; | ||
| for (const idx of freshIndices) { | ||
| const next = node.nextElementSibling; | ||
| const item = newRecord[idx]; | ||
| item.node = node; | ||
| if (item.bindings !== void 0 && item.bindings.length > 0) { | ||
| item.bindingDisposers = wireRowBindings(item.node, item.bindings); | ||
| } | ||
| node = next; | ||
| } | ||
| } | ||
| function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) { | ||
| for (let i = 0; i < freshHtmls.length; i++) { | ||
| if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) { | ||
| return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| function removeOldNodes(liveParent, removedItems) { | ||
| for (const item of removedItems) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (item.node.parentElement === liveParent) liveParent.removeChild(item.node); | ||
| } | ||
| } | ||
| function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) { | ||
| let nextSibling = tailAnchor; | ||
| for (let i = newRecord.length - 1; i >= 0; i--) { | ||
| const node = newRecord[i].node; | ||
| if (prevIdx[i] === -1 || !stable.has(i)) { | ||
| moveNode(liveParent, node, nextSibling); | ||
| } | ||
| nextSibling = node; | ||
| } | ||
| } | ||
| function lis(arr) { | ||
| const tails = []; | ||
| const tailIdx = []; | ||
| const prev = new Array(arr.length); | ||
| for (let i = 0; i < arr.length; i++) { | ||
| const v = arr[i]; | ||
| if (v === -1) { | ||
| prev[i] = -1; | ||
| continue; | ||
| } | ||
| let lo = 0; | ||
| let hi = tails.length; | ||
| while (lo < hi) { | ||
| const mid = lo + hi >> 1; | ||
| if (tails[mid] < v) lo = mid + 1; | ||
| else hi = mid; | ||
| } | ||
| prev[i] = lo > 0 ? tailIdx[lo - 1] : -1; | ||
| tails[lo] = v; | ||
| tailIdx[lo] = i; | ||
| } | ||
| const out = /* @__PURE__ */ new Set(); | ||
| let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1; | ||
| while (k !== -1) { | ||
| out.add(k); | ||
| k = prev[k]; | ||
| } | ||
| return out; | ||
| } | ||
| // src/list-reconcile.ts | ||
| function reconcileList(binding, listSeg) { | ||
| if (listSeg.patches !== void 0 && binding.items.length > 0) { | ||
| reconcileGranular(binding, listSeg.patches); | ||
| return; | ||
| } | ||
| reconcileSnapshot(binding, listSeg); | ||
| } | ||
| // src/mount.ts | ||
| var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted"); | ||
| 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."; | ||
| function isMounted(el) { | ||
| return el[MOUNTED_MARKER] === true; | ||
| } | ||
| function setMounted(el, on) { | ||
| if (on) { | ||
| el[MOUNTED_MARKER] = true; | ||
| } else { | ||
| delete el[MOUNTED_MARKER]; | ||
| } | ||
| } | ||
| function describeEl(el) { | ||
| const tag = el.tagName.toLowerCase(); | ||
| const id = el.id ? `#${el.id}` : ""; | ||
| return `<${tag}${id}>`; | ||
| } | ||
| function assertNotInsideMountedTree(rootEl) { | ||
| if (isMounted(rootEl)) { | ||
| throw new Error( | ||
| `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.` | ||
| ); | ||
| } | ||
| let ancestor = rootEl.parentElement; | ||
| while (ancestor !== null) { | ||
| if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG); | ||
| ancestor = ancestor.parentElement; | ||
| } | ||
| const stack = []; | ||
| for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]); | ||
| while (stack.length > 0) { | ||
| const cur = stack.pop(); | ||
| if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG); | ||
| for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]); | ||
| } | ||
| } | ||
| function mount(rootEl, render) { | ||
| if (rootEl == null) { | ||
| throw new Error( | ||
| '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.' | ||
| ); | ||
| } | ||
| const owner = rootEl.ownerDocument; | ||
| if (owner !== document) { | ||
| if (owner.defaultView === null) document.adoptNode(rootEl); | ||
| } | ||
| assertNotInsideMountedTree(rootEl); | ||
| setMounted(rootEl, true); | ||
| const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null; | ||
| const bindings = /* @__PURE__ */ new Map(); | ||
| const renderCtx = { | ||
| counter: 0, | ||
| caches: /* @__PURE__ */ new Map(), | ||
| bindingCounts: /* @__PURE__ */ new Map(), | ||
| bindingSources: /* @__PURE__ */ new Map(), | ||
| keysThisRender: /* @__PURE__ */ new Set(), | ||
| shiftCandidates: [], | ||
| warnedShiftIds: /* @__PURE__ */ new Set(), | ||
| rebuiltLists: /* @__PURE__ */ new Set() | ||
| }; | ||
| const bindingCtx = newBindingContext(); | ||
| let bindingDisposers = []; | ||
| let prevWiredBindings = []; | ||
| let isFirst = true; | ||
| let prevStaticHtml = ""; | ||
| const valueOnlyWarnCtx = { warned: false }; | ||
| const runRenderPass = () => { | ||
| renderCtx.counter = 0; | ||
| renderCtx.keysThisRender.clear(); | ||
| renderCtx.shiftCandidates.length = 0; | ||
| bindingCtx.counter = 0; | ||
| bindingCtx.list = []; | ||
| _setRenderContext(renderCtx); | ||
| _setBindingContext(bindingCtx); | ||
| try { | ||
| return render(); | ||
| } finally { | ||
| _setRenderContext(null); | ||
| _setBindingContext(null); | ||
| } | ||
| }; | ||
| const disposeEffect = effect(() => { | ||
| let result = runRenderPass(); | ||
| const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter; | ||
| if (countChanged) { | ||
| for (const id of renderCtx.shiftCandidates) { | ||
| if (renderCtx.warnedShiftIds.has(id)) continue; | ||
| renderCtx.warnedShiftIds.add(id); | ||
| devHooks.listIdShift?.(id); | ||
| } | ||
| _resetCallOrderListState(renderCtx); | ||
| result = runRenderPass(); | ||
| } | ||
| let segment = resultToSegment(result); | ||
| if (isFirst) { | ||
| runFirstRender(rootEl, segment, bindings); | ||
| prevStaticHtml = flattenWithoutListItems(segment); | ||
| devHooks.parserRepair?.(prevStaticHtml); | ||
| bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers); | ||
| if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list; | ||
| isFirst = false; | ||
| } else { | ||
| let nextStaticHtml = runSubsequentRender( | ||
| rootEl, | ||
| segment, | ||
| bindings, | ||
| renderCtx, | ||
| prevStaticHtml, | ||
| valueOnlyWarnCtx | ||
| ); | ||
| if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) { | ||
| for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id); | ||
| result = runRenderPass(); | ||
| segment = resultToSegment(result); | ||
| nextStaticHtml = runSubsequentRender( | ||
| rootEl, | ||
| segment, | ||
| bindings, | ||
| renderCtx, | ||
| prevStaticHtml, | ||
| valueOnlyWarnCtx | ||
| ); | ||
| } | ||
| if (nextStaticHtml !== prevStaticHtml) { | ||
| bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers); | ||
| if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list; | ||
| } else { | ||
| devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list); | ||
| } | ||
| prevStaticHtml = nextStaticHtml; | ||
| } | ||
| const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null; | ||
| for (const listSeg of collectLists(segment).values()) { | ||
| const binding = bindings.get(listSeg.id); | ||
| if (binding === void 0) { | ||
| throw new Error( | ||
| "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." | ||
| ); | ||
| } | ||
| reconcileList(binding, listSeg); | ||
| renderCtx.bindingCounts.set(listSeg.id, binding.items.length); | ||
| renderCtx.bindingSources.set(listSeg.id, listSeg.source); | ||
| expectedCounts?.set( | ||
| listSeg.id, | ||
| listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length | ||
| ); | ||
| } | ||
| renderCtx.previousCallCount = renderCtx.counter; | ||
| devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0); | ||
| }); | ||
| return () => { | ||
| disposeEffect(); | ||
| for (const d of bindingDisposers) d(); | ||
| bindingDisposers = []; | ||
| for (const b of bindings.values()) { | ||
| for (const item of b.items) disposeRowBindings(item.bindingDisposers); | ||
| } | ||
| listenerWarnObserver?.disconnect(); | ||
| setMounted(rootEl, false); | ||
| }; | ||
| } | ||
| function runFirstRender(rootEl, segment, bindings) { | ||
| rootEl.innerHTML = flatten(segment, true); | ||
| bindListsFromMarkers(rootEl, segment, bindings, true); | ||
| } | ||
| function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) { | ||
| renderCtx.rebuiltLists.clear(); | ||
| const currentStaticHtml = flattenWithoutListItems(segment); | ||
| if (currentStaticHtml === prevStaticHtml) { | ||
| return prevStaticHtml; | ||
| } | ||
| devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx); | ||
| cleanupOrphanBindings(segment, bindings, renderCtx); | ||
| const template = rootEl.cloneNode(false); | ||
| template.innerHTML = currentStaticHtml; | ||
| morph(rootEl, template, collectOwnedItems(bindings)); | ||
| bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists); | ||
| return currentStaticHtml; | ||
| } | ||
| function coerceRenderResult(result) { | ||
| if (result === null || result === void 0) return ""; | ||
| if (result === false || result === true) return ""; | ||
| return String(result); | ||
| } | ||
| function resultToSegment(result) { | ||
| return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) }; | ||
| } | ||
| function anyRebuiltListIsGranular(segment, rebuilt) { | ||
| if (rebuilt.size === 0) return false; | ||
| const lists = collectLists(segment); | ||
| for (const id of rebuilt) { | ||
| if (lists.get(id)?.patches !== void 0) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) { | ||
| const lists = collectLists(segment); | ||
| const found = []; | ||
| collectComments(rootEl, found); | ||
| for (const marker of found) { | ||
| if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue; | ||
| const id = marker.data.slice(LIST_MARKER_PREFIX.length); | ||
| const existing = bindings.get(id); | ||
| if (existing !== void 0) { | ||
| if (existing.marker === marker && rootEl.contains(existing.marker)) continue; | ||
| for (const item of existing.items) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (rootEl.contains(item.node)) { | ||
| item.node.parentElement?.removeChild(item.node); | ||
| } | ||
| } | ||
| bindings.delete(id); | ||
| rebuiltLists?.add(id); | ||
| devHooks.listRebind?.(id, marker.parentElement); | ||
| } | ||
| const listSeg = lists.get(id); | ||
| const liveParent = marker.parentElement; | ||
| const items = []; | ||
| if (inlinedItems) { | ||
| let next = marker.nextElementSibling; | ||
| for (let i = 0; i < listSeg.items.length && next !== null; i++) { | ||
| validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent); | ||
| const rowBindings = listSeg.items[i].bindings; | ||
| const bound = { | ||
| ref: listSeg.items[i].ref, | ||
| cacheKey: listSeg.items[i].cacheKey, | ||
| html: listSeg.items[i].html, | ||
| node: next, | ||
| bindings: rowBindings | ||
| }; | ||
| if (rowBindings !== void 0 && rowBindings.length > 0) { | ||
| bound.bindingDisposers = wireRowBindings(next, rowBindings); | ||
| } | ||
| items.push(bound); | ||
| next = next.nextElementSibling; | ||
| } | ||
| } | ||
| const binding = { liveParent, items, marker }; | ||
| if (items.length > 0) { | ||
| devHooks.missingRowKey?.(items[0].node, items[0].html, binding); | ||
| } | ||
| devHooks.eachInMorphSkip?.(id, liveParent, rootEl); | ||
| bindings.set(id, binding); | ||
| } | ||
| } | ||
| function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) { | ||
| if (boundEl.outerHTML === expectedHtml) return; | ||
| const { content, count } = parseRowTemplate(expectedHtml, liveParent); | ||
| if (count !== 1) throw rowContractError(index, expectedHtml, liveParent); | ||
| const expectedTag = content.firstElementChild.tagName; | ||
| if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag); | ||
| } | ||
| function rowStructureError(index, gotTag, wantTag) { | ||
| const got = gotTag.toLowerCase(); | ||
| const want = wantTag.toLowerCase(); | ||
| return new Error( | ||
| `each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.` | ||
| ); | ||
| } | ||
| function collectOwnedItems(bindings) { | ||
| const owned = /* @__PURE__ */ new Set(); | ||
| for (const b of bindings.values()) { | ||
| for (const item of b.items) owned.add(item.node); | ||
| } | ||
| return owned; | ||
| } | ||
| function cleanupOrphanBindings(segment, bindings, renderCtx) { | ||
| const liveIds = collectLists(segment); | ||
| for (const [id, binding] of bindings) { | ||
| if (liveIds.has(id)) continue; | ||
| for (const item of binding.items) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (item.node.parentElement !== null) { | ||
| item.node.parentElement.removeChild(item.node); | ||
| } | ||
| } | ||
| if (binding.marker.parentElement !== null) { | ||
| binding.marker.parentElement.removeChild(binding.marker); | ||
| } | ||
| bindings.delete(id); | ||
| renderCtx.bindingCounts.delete(id); | ||
| renderCtx.bindingSources.delete(id); | ||
| renderCtx.caches.delete(id); | ||
| } | ||
| } | ||
| function collectComments(node, out) { | ||
| for (let c = node.firstChild; c !== null; c = c.nextSibling) { | ||
| if (c.nodeType === Node.COMMENT_NODE) out.push(c); | ||
| else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out); | ||
| } | ||
| } | ||
| export { each, morph, mount, moveNode }; | ||
| //# sourceMappingURL=chunk-SRWQKB33.js.map | ||
| //# sourceMappingURL=chunk-SRWQKB33.js.map |
Sorry, the diff of this file is too big to display
+1
-1
@@ -1,2 +0,2 @@ | ||
| export { each, morph, mount } from './chunk-LKWAKC2X.js'; | ||
| export { each, morph, mount } from './chunk-SRWQKB33.js'; | ||
| export { defineStore, resetAllStores } from './chunk-SAYPJ6XR.js'; | ||
@@ -3,0 +3,0 @@ export { attr } from './chunk-U32TFTGZ.js'; |
+35
-0
@@ -121,2 +121,30 @@ import { M as MountResult } from './mount-Bo2qOx25.js'; | ||
| * `clientHeight` 0) fills in once it's sized, and a resized container re-windows. | ||
| * | ||
| * `mode` (default `'window'`) picks the virtualization STRATEGY: | ||
| * - **`'window'`** — the JS windowing above: only the visible rows are in the | ||
| * DOM, bounded node count, works on every engine. Off-window rows are removed | ||
| * (see the findability tradeoff below). | ||
| * - **`'content-visibility'`** — **every** row stays in the DOM and kerf sets | ||
| * `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on | ||
| * each one, so a supporting engine (Chromium, Safari 18) skips the *layout / | ||
| * paint* of off-screen rows while keeping them findable. `rowHeight` here is | ||
| * used **only** as the `contain-intrinsic-size` placeholder (scrollbar | ||
| * accuracy before a row is first rendered) — there is no windowing math, no | ||
| * padding, no scroll listener, no anchor correction, and `setHeight` / | ||
| * `observeRowHeights` are **no-ops** (the browser owns real measurement). | ||
| * `minRows` is ignored (all rows already render). On an engine without | ||
| * `content-visibility` the CSS is simply inert — all rows render, correct and | ||
| * fully findable, only without the skip optimization. Choose this mode for | ||
| * medium lists where find-in-page / a11y / anchor links matter more than the | ||
| * node ceiling; keep `'window'` for very large (100k-row) lists. | ||
| * | ||
| * **Findability tradeoff (`'window'` mode).** Off-window rows are removed from | ||
| * the DOM (not merely hidden), so with `mode: 'window'`: **find-in-page | ||
| * (Cmd/Ctrl+F)**, **screen readers / the a11y tree**, and **anchor links / | ||
| * `scrollIntoView`** only reach the visible window — a match, an announced row, | ||
| * or a linked element that has been windowed out isn't in the DOM to find. | ||
| * Convey the true total via ARIA (`aria-rowcount` / `aria-setsize`) if it | ||
| * matters, and use a non-virtualized list — `minRows` above the list length, or | ||
| * `mode: 'content-visibility'` — when full findability matters more than the DOM | ||
| * node ceiling. See `docs/17-list-virtualization.md` §17.10 / §17.11. | ||
| */ | ||
@@ -129,2 +157,9 @@ virtualize?: { | ||
| containerId?: string; | ||
| /** | ||
| * Virtualization strategy. `'window'` (default) removes off-window rows from | ||
| * the DOM; `'content-visibility'` keeps every row in the DOM and lets the | ||
| * browser skip off-screen layout/paint (full find-in-page / a11y, at the cost | ||
| * of an unbounded node count). See the option JSDoc above. | ||
| */ | ||
| mode?: 'window' | 'content-visibility'; | ||
| }; | ||
@@ -131,0 +166,0 @@ } |
+26
-13
| import { ARRAY_SIGNAL_BRAND } from './chunk-MRYM3O3V.js'; | ||
| import { mount } from './chunk-LKWAKC2X.js'; | ||
| import { moveNode, mount } from './chunk-SRWQKB33.js'; | ||
| import './chunk-QIP723L4.js'; | ||
@@ -15,2 +15,3 @@ import './chunk-YHH7OUFA.js'; | ||
| const minRows = virtualize?.minRows; | ||
| const contentVisibility = virtualize?.mode === "content-visibility"; | ||
| const endAnchor = () => { | ||
@@ -94,3 +95,3 @@ if (virtualize !== void 0 || before === void 0) return null; | ||
| if (el.parentNode !== container || el.nextSibling !== ref) { | ||
| container.insertBefore(el, ref); | ||
| moveNode(container, el, ref); | ||
| } | ||
@@ -115,3 +116,3 @@ ref = el; | ||
| order.splice(patch.to, 0, row); | ||
| container.insertBefore(row.el, order[patch.to + 1]?.el ?? endAnchor()); | ||
| moveNode(container, row.el, order[patch.to + 1]?.el ?? endAnchor()); | ||
| } else if (patch.type === "update") { | ||
@@ -154,2 +155,3 @@ const current = order[patch.index]; | ||
| } : (index) => rowHeight(items[index], index); | ||
| const intrinsicSizeAt = (index) => fixedHeight !== null ? fixedHeight : variableHeightAt(index); | ||
| let offsets = [0]; | ||
@@ -215,2 +217,11 @@ let heightsDirty = true; | ||
| } | ||
| if (contentVisibility) { | ||
| syncRows(items); | ||
| for (let j = 0; j < order.length; j++) { | ||
| const el = order[j].el; | ||
| el.style.contentVisibility = "auto"; | ||
| el.style.containIntrinsicSize = `0 ${intrinsicSizeAt(j)}px`; | ||
| } | ||
| return; | ||
| } | ||
| const total = items.length; | ||
@@ -273,8 +284,8 @@ let start; | ||
| }; | ||
| if (virtualize !== void 0) parent.addEventListener("scroll", scheduleRender); | ||
| if (virtualize !== void 0 && !contentVisibility) parent.addEventListener("scroll", scheduleRender); | ||
| const RO = globalThis.ResizeObserver; | ||
| const parentResize = virtualize !== void 0 && RO !== void 0 ? new RO(scheduleRender) : void 0; | ||
| const parentResize = virtualize !== void 0 && !contentVisibility && RO !== void 0 ? new RO(scheduleRender) : void 0; | ||
| parentResize?.observe(parent); | ||
| const setHeight = (k, height) => { | ||
| if (!measuring) return; | ||
| if (!measuring || contentVisibility) return; | ||
| const idx = indexByKey.get(k); | ||
@@ -309,9 +320,11 @@ if (idx === void 0) return; | ||
| handle.container = container; | ||
| VIRTUAL_INTERNALS.set(handle, { | ||
| visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })), | ||
| onRender: (cb) => { | ||
| renderSubscribers.add(cb); | ||
| return () => renderSubscribers.delete(cb); | ||
| } | ||
| }); | ||
| if (!contentVisibility) { | ||
| VIRTUAL_INTERNALS.set(handle, { | ||
| visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })), | ||
| onRender: (cb) => { | ||
| renderSubscribers.add(cb); | ||
| return () => renderSubscribers.delete(cb); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
@@ -318,0 +331,0 @@ return handle; |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/list.ts"],"names":["dispose"],"mappings":"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","file":"list.js","sourcesContent":["/**\n * `kerfjs/list` — `bindList`, a keyed list with a live per-row mount and\n * optional viewport virtualization.\n *\n * This is a DELIBERATE second list API, distinct from `each()`. It does two\n * things `each()` structurally cannot:\n * 1. **Per-row reactivity.** Every row is individually `mount()`ed, so a signal\n * the row's `render` reads updates just that row (fine-grained binding or a\n * one-row morph) without touching its siblings — no full-list pass.\n * 2. **Virtualization.** With `{ virtualize: { rowHeight } }` only the rows in\n * the scroll viewport are rendered; padding on the scroll container keeps\n * `scrollHeight` honest. `rowHeight` is a fixed `number` (O(1) windowing), a\n * `(item, index) => number` for **app-declared variable** heights (a prefix\n * sum + binary-search window), or `{ estimate }` for **measured** heights —\n * the app reports real heights via the returned handle's `setHeight` (or the\n * `observeRowHeights` helper) and kerf anchor-corrects `scrollTop`. See\n * `docs/17-list-virtualization.md`.\n *\n * `each()` stays the choice for item-owned-state lists rendered to HTML strings;\n * reach for `bindList` when you need surgical per-row updates or windowing.\n *\n * import { bindList } from 'kerfjs/list';\n *\n * const dispose = bindList(listEl, itemsSignal, {\n * key: (row) => row.id,\n * render: (row) => <span class={selected} data-id={row.id}>{row.label}</span>,\n * tag: 'li',\n * virtualize: { rowHeight: 32 },\n * });\n *\n * `render` reads signals for reactivity (external state like a `selectedId`, or\n * signals the item carries) — keep the item OBJECTS stable across renders and\n * drive structure (add/remove/move) through `itemsSignal`. A row whose item\n * object identity changes is rebuilt (same rule as `each()`'s memo). `bindList`\n * OWNS `parent`'s children by default (append/move to the end) — to share\n * `parent` with fixed trailing siblings (an \"add\" button, an indicator), pass\n * `before` so the rows end just before that node. It reads `itemsSignal.value`,\n * so a plain `signal<T[]>` or an `arraySignal<T>` both work.\n */\nimport { ARRAY_SIGNAL_BRAND, type ArrayPatch } from './array-signal.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A row's stable key. */\nexport type ListKey = string | number;\n\n/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */\nexport interface ListSource<T> {\n readonly value: readonly T[];\n}\n\n/**\n * A row built imperatively by `render`: return the row **element** itself (kerf\n * keys/moves/reuses it and owns nothing inside it), or `{ el, update?, dispose? }`\n * to also hand back an `update(item)` — called on the SAME element when the row's\n * key persists but its item changes — and a `dispose` that runs only when the row\n * is removed.\n */\nexport type RowElement<T> =\n | HTMLElement\n | { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n\n/**\n * The virtualization height model:\n * - **`number`** — every row is this fixed pixel height (O(1) windowing).\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index.\n * - **`{ estimate }`** — **measured** heights: kerf uses `estimate` for a row\n * until the app reports its real height through {@link BindListHandle.setHeight}\n * (or the `observeRowHeights` helper). See `docs/17-list-virtualization.md`.\n */\nexport type RowHeight<T> =\n | number\n | ((item: T, index: number) => number)\n | { estimate: number | ((item: T, index: number) => number) };\n\n/**\n * The value {@link bindList} returns: a disposer you call to tear the list down,\n * augmented with `setHeight` for the **measured** virtualization mode.\n */\nexport type BindListHandle = (() => void) & {\n /**\n * Report a row's real pixel height (measured after layout) for\n * `virtualize: { rowHeight: { estimate } }` lists. Keyed by the list `key`, so\n * a report survives reorders. kerf recomputes the window and, if the row sits\n * ABOVE the viewport, anchor-corrects `scrollTop` so content doesn't jump.\n * A no-op for fixed / declared-height lists and for unknown keys.\n */\n setHeight: (key: ListKey, height: number) => void;\n /**\n * The inner container element kerf creates to hold the rows in a **virtualized**\n * list (the \"sizer\"). `undefined` for a non-virtualized list (there the rows\n * live directly in `parent`). Use it to style, id, or otherwise reach the row\n * block without guessing at `parent.lastElementChild` — though `containerClass`\n * / `containerId` on `virtualize` set those declaratively.\n */\n container?: HTMLElement;\n};\n\n/** Options for {@link bindList}. */\nexport interface BindListOptions<T> {\n /** Stable per-row key. Rows are matched, moved, and reused by this. */\n key: (item: T) => ListKey;\n /**\n * Build a row. Two modes, chosen per call by what you return:\n * - **Content mode** (a `MountResult` — JSX / `SafeHtml`): kerf creates the\n * row element (`tag`) and `mount()`s your content inside it, so signals your\n * content reads drive per-row reactivity.\n * - **Element mode** (an `HTMLElement`, or `{ el, update?, dispose? }`): the\n * element you return IS the row, so you own its tag, class, `data-*`, and\n * listeners. kerf **keys/moves/reuses** it — the SAME element survives an\n * append/remove/reorder or a fresh item object at the same key. Refresh its\n * content by reading signals inside it, or by returning an `update(item)`\n * that kerf calls on the existing element when the item changes. `dispose`\n * runs only when the row is genuinely removed.\n */\n render: (item: T) => MountResult | RowElement<T>;\n /** Row element tag for **content mode**. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). Ignored in element mode. */\n tag?: string;\n /**\n * Keep the rows as a contiguous block that ENDS just before this node, instead\n * of at the very end of `parent`. Use it when `parent` also holds non-row\n * siblings that must stay put — a trailing \"add\" button, a sliding indicator:\n * `before: () => addButton`. The node (a function is re-read each reconcile, or\n * pass the node directly) must be a child of `parent`. Without it, bindList\n * assumes exclusive ownership and appends rows to the end. Ignored when\n * virtualized (the rows own bindList's inner sizer exclusively).\n */\n before?: Node | (() => Node | null);\n /**\n * Turn on viewport virtualization. `parent` must be a scroll container (your\n * CSS: a fixed height + `overflow: auto`). `overscan` (default 3) is how many\n * extra rows to render above and below the viewport.\n *\n * `rowHeight` (a {@link RowHeight}) is the height model:\n * - **`number`** — every row is this fixed pixel height. O(1) windowing, no\n * cumulative model built.\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index. kerf builds a prefix sum of the\n * heights (rebuilt when the source array changes, not per scroll frame) and\n * binary-searches it to find the visible window. Return a non-negative\n * number of pixels.\n * - **`{ estimate }`** — **measured** heights for rows whose height is only\n * known after layout. kerf sizes an unmeasured row by `estimate` (a number\n * or an `(item, index) => number`), and the app reports each row's real\n * height via {@link BindListHandle.setHeight} (or the `observeRowHeights`\n * helper). kerf anchor-corrects `scrollTop` when an above-viewport row is\n * remeasured, so content doesn't jump.\n *\n * `minRows` renders **every** row (no windowing, zero padding) while the list\n * is shorter than it, and windows only at or above it — the DOM structure (the\n * inner container) is the same either way, so the call site never branches. A\n * fully-rendered short list is friendlier to find-in-page, screen readers, and\n * DOM-count assertions, which only see rows actually in the DOM.\n *\n * `containerClass` / `containerId` are set on the inner container kerf creates\n * to hold the rows, so it's reachable from CSS and tests without guessing at\n * `parent.lastElementChild` (it's also on the handle as `handle.container`).\n *\n * kerf re-windows on `parent`'s `scroll` and, where `ResizeObserver` exists, on\n * `parent` resizing — so a list that mounts before layout (a hidden tab,\n * `clientHeight` 0) fills in once it's sized, and a resized container re-windows.\n */\n virtualize?: {\n rowHeight: RowHeight<T>;\n overscan?: number;\n minRows?: number;\n containerClass?: string;\n containerId?: string;\n };\n}\n\ninterface Row<T> {\n el: HTMLElement;\n item: T;\n dispose: () => void;\n /** True for element-mode rows (the caller owns the element — reuse it, don't rebuild on item change). */\n elementMode: boolean;\n /** Element mode only: refresh the existing element when the item changes at the same key. */\n update?: (item: T) => void;\n}\n\n/**\n * Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a\n * `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears\n * down every row mount, the scroll listener (if virtualized), and the source\n * subscription.\n */\nexport function bindList<T>(\n parent: HTMLElement,\n source: ListSource<T>,\n options: BindListOptions<T>,\n): BindListHandle {\n const { key, render, tag = 'div', virtualize, before } = options;\n const overscan = virtualize?.overscan ?? 3;\n const minRows = virtualize?.minRows;\n\n // The node the row block ends before — `before` (KF-496) when the list shares\n // `parent` with trailing siblings, else the end of the container. Never applies\n // when virtualized: the rows own bindList's inner sizer exclusively.\n const endAnchor = (): Node | null => {\n if (virtualize !== undefined || before === undefined) return null;\n return (typeof before === 'function' ? before() : before) ?? null;\n };\n\n const rows = new Map<ListKey, Row<T>>();\n // The current DOM order of rows, kept in step by both the keyed-diff and the\n // granular patch paths so index-based patches can address rows directly.\n const order: Array<Row<T>> = [];\n let items: readonly T[] = [];\n let disposed = false;\n let rafPending = false;\n let firstRender = true;\n\n // Granular fast path (KF-478): when the source is an `arraySignal` and the\n // list is NOT virtualized, apply its insert/remove/move/update patches\n // directly in O(patches) instead of diffing the whole snapshot. Virtualized\n // lists keep the keyed diff — their visible set is just the window (cheap),\n // and absolute-index patches don't compose with a shifting window. A plain\n // `signal<T[]>` has no patches, so it always uses the keyed diff.\n const patchSource = source as {\n [ARRAY_SIGNAL_BRAND]?: boolean;\n _consumePatches?: () => ArrayPatch<T>[];\n };\n const granularEligible = virtualize === undefined && patchSource[ARRAY_SIGNAL_BRAND] === true;\n\n // Virtualized lists put the windowing padding + rows on an INNER sizer, so the\n // padding never inflates the scroll container's clientHeight (padding counts\n // toward clientHeight). `parent` stays the clean scroll viewport; `container`\n // holds the rows. Non-virtualized lists render straight into `parent`.\n const container: HTMLElement = virtualize === undefined ? parent : document.createElement('div');\n if (virtualize !== undefined) {\n if (virtualize.containerClass !== undefined) container.className = virtualize.containerClass;\n if (virtualize.containerId !== undefined) container.id = virtualize.containerId;\n parent.appendChild(container);\n }\n\n const NOOP = (): void => { /* element-mode rows with no caller teardown */ };\n\n // Detect element mode from a render result: a raw `HTMLElement`, or a\n // `{ el, dispose? }` object. Everything else (SafeHtml / string / nullish) is\n // content mode. SafeHtml is an object but has no `el`, so it never matches.\n const asElementRow = (\n rendered: MountResult | RowElement<T>,\n ): { el: HTMLElement; dispose: () => void; update?: (item: T) => void } | null => {\n if (rendered instanceof HTMLElement) return { el: rendered, dispose: NOOP };\n if (\n rendered !== null\n && typeof rendered === 'object'\n && 'el' in rendered\n && (rendered as { el: unknown }).el instanceof HTMLElement\n ) {\n const r = rendered as { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n return { el: r.el, dispose: r.dispose ?? NOOP, update: r.update };\n }\n return null;\n };\n\n const makeRow = (item: T): Row<T> => {\n // One call decides the mode per row (so a list may mix element + content rows).\n const elementRow = asElementRow(render(item));\n if (elementRow !== null) {\n // Element mode: the returned element IS the row; the caller owns its\n // content + cleanup. bindList sizes it for the windowing math per render\n // (see `sizeVisibleRows`), not here, since a variable height depends on the\n // row's current index in the full list.\n return { el: elementRow.el, item, dispose: elementRow.dispose, elementMode: true, update: elementRow.update };\n }\n // Content mode: kerf creates the row element and mounts `render` inside it,\n // so the content is per-row reactive. (In content mode `render` runs once\n // more here for the mode probe than the mount itself needs — keep it a pure\n // projection, which bindList already requires.)\n const el = document.createElement(tag);\n // Content mode: `render` returns a MountResult here (element results were\n // handled above), so narrowing it for `mount` is sound.\n const dispose = mount(el, () => render(item) as MountResult);\n return { el, item, dispose, elementMode: false };\n };\n\n // A row whose KEY persists but whose item object changed. Content-mode rows are\n // rebuilt (their mount re-renders the fresh item); element-mode rows are REUSED\n // — the caller owns the element, so we keep it (preserving focus / scroll /\n // listeners) and refresh via the optional `update(item)`. Returns the row to\n // use at that key (a fresh one for content, the same one for element).\n const reconcileItem = (row: Row<T>, k: ListKey, item: T): Row<T> => {\n if (row.item === item) return row;\n if (row.elementMode) {\n row.item = item;\n row.update?.(item);\n return row;\n }\n row.dispose();\n row.el.remove();\n rows.delete(k);\n const fresh = makeRow(item);\n rows.set(k, fresh);\n return fresh;\n };\n\n // Reconcile the live rows to exactly `visible`, in order, keyed.\n const syncRows = (visible: readonly T[]): void => {\n const wanted = new Set<ListKey>();\n for (const item of visible) wanted.add(key(item));\n\n // Remove rows that are gone from the window.\n for (const [k, row] of rows) {\n if (!wanted.has(k)) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n }\n }\n\n // Create missing rows; reuse existing ones by key (element rows keep their\n // element across item changes; content rows rebuild on identity change).\n order.length = 0;\n for (const item of visible) {\n const k = key(item);\n const existing = rows.get(k);\n let row: Row<T>;\n if (existing !== undefined) {\n row = reconcileItem(existing, k, item);\n } else {\n row = makeRow(item);\n rows.set(k, row);\n }\n order.push(row);\n }\n\n // Reverse pass: move only rows that are out of position.\n let ref: Node | null = endAnchor();\n for (let i = order.length - 1; i >= 0; i--) {\n const el = order[i].el;\n if (el.parentNode !== container || el.nextSibling !== ref) {\n container.insertBefore(el, ref);\n }\n ref = el;\n }\n };\n\n // Apply arraySignal structural patches directly to `order` + the DOM, in\n // O(patches). Indices are always valid by construction: `order` reflects the\n // last-rendered state and the patches are exactly the delta from it (bindList\n // drains the queue every render, and `replace` is filtered out by the caller,\n // which snapshots instead). The `splice()`s mirror `arraySignal`'s own\n // `_items` mutations exactly.\n const applyPatches = (patches: readonly ArrayPatch<T>[]): void => {\n for (const patch of patches) {\n if (patch.type === 'insert') {\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order.splice(patch.index, 0, row);\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n } else if (patch.type === 'remove') {\n const [row] = order.splice(patch.index, 1);\n row.dispose();\n row.el.remove();\n rows.delete(key(row.item));\n } else if (patch.type === 'move') {\n const [row] = order.splice(patch.from, 1);\n order.splice(patch.to, 0, row);\n container.insertBefore(row.el, order[patch.to + 1]?.el ?? endAnchor());\n } else if (patch.type === 'update') {\n // An item whose OBJECT identity changed: content rows rebuild (their mount\n // re-renders the fresh item); element rows are REUSED — keep the caller's\n // element and refresh via update(), re-keying if the key changed. A\n // same-ref update needs nothing (the row's mount reacts to its signals).\n const current = order[patch.index];\n if (current.item !== patch.item) {\n if (current.elementMode) {\n const oldKey = key(current.item);\n const newKey = key(patch.item);\n current.item = patch.item;\n if (newKey !== oldKey) {\n rows.delete(oldKey);\n rows.set(newKey, current);\n }\n current.update?.(patch.item);\n } else {\n current.dispose();\n current.el.remove();\n rows.delete(key(current.item));\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order[patch.index] = row;\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n }\n }\n }\n // 'replace' never reaches here — the caller snapshots on it.\n }\n };\n\n // Virtualization height model, three modes:\n // - `fixedHeight` (a `number`): the O(1) fast path — no cumulative model.\n // - `variableHeightAt` (a function): app-declared per-row heights.\n // - measuring (`{ estimate }`): `variableHeightAt` returns the measured height\n // when the app has reported one (via `setHeight`), else the estimate.\n // In the two variable cases, `offsets[i]` is the total height of rows 0..i-1\n // (a prefix sum, length total+1), so `offsets[i+1] - offsets[i]` is row i's\n // height and `offsets[total]` is the full scroll height. It is rebuilt only\n // when `items` changes or a height is reported (heightsDirty), never per scroll\n // frame — a scroll reuses the prefix sum and pays only the O(log n) searches.\n const rowHeight = virtualize?.rowHeight;\n const fixedHeight = typeof rowHeight === 'number' ? rowHeight : null;\n const measuring = typeof rowHeight === 'object' && rowHeight !== null;\n const measured = new Map<ListKey, number>(); // key → real reported height\n const estimateAt = (index: number): number => {\n const est = (rowHeight as { estimate: number | ((item: T, index: number) => number) }).estimate;\n return typeof est === 'function' ? est(items[index], index) : est;\n };\n const variableHeightAt: ((index: number) => number) | null =\n fixedHeight !== null\n ? null\n : measuring\n ? (index): number => {\n const k = key(items[index]);\n return measured.has(k) ? (measured.get(k) as number) : estimateAt(index);\n }\n : (index): number => (rowHeight as (item: T, index: number) => number)(items[index], index);\n\n let offsets: number[] = [0];\n let heightsDirty = true;\n // Measuring only: key → current absolute index, so `setHeight(key, …)` locates\n // the row in O(1). Rebuilt with the prefix sum when `items` changes.\n const indexByKey = new Map<ListKey, number>();\n // Accumulated scroll-anchor correction: the summed height delta of remeasured\n // rows that sit entirely ABOVE the viewport top, applied to `scrollTop` before\n // the next window render so on-screen content does not jump.\n let pendingAnchorDelta = 0;\n\n const rebuildOffsets = (): void => {\n const fn = variableHeightAt as (index: number) => number;\n const total = items.length;\n offsets = new Array<number>(total + 1);\n offsets[0] = 0;\n if (measuring) indexByKey.clear();\n for (let i = 0; i < total; i++) {\n offsets[i + 1] = offsets[i] + fn(i);\n if (measuring) indexByKey.set(key(items[i]), i);\n }\n // Prune reported heights for keys no longer in the source, so a measured list\n // with key churn (a feed prepending new ids over a long session) doesn't grow\n // `measured` without bound. `indexByKey` now holds exactly the live keys (all\n // of them, windowed or not, since we walked every item). A key that only\n // scrolled out of the window stays — it's still in the source.\n if (measuring) {\n for (const k of measured.keys()) if (!indexByKey.has(k)) measured.delete(k);\n }\n };\n\n // Greatest index i in [0, total] with `offsets[i] <= target` — the first row\n // whose top is at or above `target` (the viewport top).\n const findStart = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (offsets[mid] <= target) lo = mid;\n else hi = mid - 1;\n }\n return lo;\n };\n\n // Smallest index i in [0, total] with `offsets[i] >= target` — one past the\n // last row that starts before `target` (the viewport bottom). `total` if none.\n const findEnd = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (offsets[mid] >= target) hi = mid;\n else lo = mid + 1;\n }\n return lo;\n };\n\n // Size each visible row for the windowing math. `order` holds the visible rows\n // in order, so `order[j]` is the item at absolute index `start + j`.\n // MEASURED mode is the exception: the row must take its NATURAL height so the\n // app (or `observeRowHeights`) can read the real `offsetHeight` — forcing a\n // height here would make the measurement echo the estimate. Its offsets come\n // from `setHeight` reports instead.\n const sizeVisibleRows = (start: number): void => {\n if (measuring) return;\n for (let j = 0; j < order.length; j++) {\n const abs = start + j;\n const h = fixedHeight !== null ? fixedHeight : offsets[abs + 1] - offsets[abs];\n order[j].el.style.height = `${h}px`;\n }\n };\n\n // Called after each virtualized window render (used by `observeRowHeights` to\n // re-observe the current visible rows).\n const renderSubscribers = new Set<() => void>();\n\n const renderWindow = (): void => {\n if (virtualize === undefined) {\n if (granularEligible) {\n // Always drain to keep the single patch queue clean (so patches never\n // double-apply). Take the granular path past the first render, when\n // there are patches, and none is a `replace` (which reshapes the whole\n // array — snapshot instead). Otherwise fall through to a keyed diff.\n const patches = patchSource._consumePatches!();\n if (\n !firstRender\n && patches.length > 0\n && !patches.some((p) => p.type === 'replace')\n ) {\n applyPatches(patches);\n return;\n }\n }\n syncRows(items);\n firstRender = false;\n return;\n }\n const total = items.length;\n let start: number;\n let end: number;\n let padTop: number;\n let padBottom: number;\n if (minRows !== undefined && total < minRows) {\n // Below the threshold: render EVERY row, no windowing, zero padding — one\n // DOM structure (the inner container) shared with the windowed path, so the\n // caller never branches. Rows are still sized (declared/fixed) from the\n // prefix sum, which we still build for the sizing pass.\n if (fixedHeight === null && heightsDirty) {\n rebuildOffsets();\n heightsDirty = false;\n }\n start = 0;\n end = total;\n padTop = 0;\n padBottom = 0;\n } else {\n const scrollTop = parent.scrollTop;\n const viewportBottom = scrollTop + parent.clientHeight;\n if (fixedHeight !== null) {\n start = Math.max(0, Math.floor(scrollTop / fixedHeight) - overscan);\n end = Math.min(total, Math.ceil(viewportBottom / fixedHeight) + overscan);\n padTop = start * fixedHeight;\n padBottom = Math.max(0, total - end) * fixedHeight;\n } else {\n if (heightsDirty) {\n rebuildOffsets();\n heightsDirty = false;\n }\n start = Math.max(0, findStart(scrollTop, total) - overscan);\n end = Math.min(total, findEnd(viewportBottom, total) + overscan);\n padTop = offsets[start];\n padBottom = offsets[total] - offsets[end];\n }\n }\n syncRows(items.slice(start, end));\n sizeVisibleRows(start);\n container.style.paddingTop = `${padTop}px`;\n container.style.paddingBottom = `${padBottom}px`;\n for (const cb of renderSubscribers) cb();\n };\n\n const stopEffect = effect(() => {\n items = source.value; // tracking read — re-runs on any structural change\n heightsDirty = true; // items changed → the prefix sum (if any) is stale\n renderWindow();\n });\n\n // One rAF-coalesced render, shared by scroll and by measurement reports. A\n // pending anchor correction is applied to `scrollTop` first (which itself may\n // fire a scroll, but with the delta already cleared the follow-up is a no-op).\n const scheduleRender = (): void => {\n if (rafPending) return;\n rafPending = true;\n globalThis.requestAnimationFrame(() => {\n rafPending = false;\n if (disposed) return;\n if (pendingAnchorDelta !== 0) {\n parent.scrollTop += pendingAnchorDelta;\n pendingAnchorDelta = 0;\n }\n renderWindow();\n });\n };\n if (virtualize !== undefined) parent.addEventListener('scroll', scheduleRender);\n\n // Re-window when `parent` RESIZES, not just on scroll. This makes two cases\n // robust that the scroll-only model missed: a list mounted before layout\n // (`clientHeight` 0 — a hidden tab, pre-first-paint) fills in once it's sized,\n // and a container resized while open re-windows. ResizeObserver fires an\n // initial callback on observe, so the 0-height case self-heals with no synthetic\n // scroll. Absent (older SSR/runtime) → scroll-only, as before.\n const RO = globalThis.ResizeObserver;\n const parentResize = virtualize !== undefined && RO !== undefined ? new RO(scheduleRender) : undefined;\n parentResize?.observe(parent);\n\n // Measured mode: report a row's real height. No-op for fixed / declared lists\n // and for keys not currently in the list.\n const setHeight = (k: ListKey, height: number): void => {\n if (!measuring) return;\n const idx = indexByKey.get(k);\n if (idx === undefined) return;\n const oldHeight = measured.has(k) ? (measured.get(k) as number) : estimateAt(idx);\n if (height === oldHeight) return;\n measured.set(k, height);\n // A row whose bottom is at/above the viewport top shifts everything below it\n // (the on-screen content) by the height delta — correct `scrollTop` to match.\n // Uses the CURRENT (pre-rebuild) offsets, which reflect the on-screen layout.\n if (offsets[idx + 1] <= parent.scrollTop) pendingAnchorDelta += height - oldHeight;\n heightsDirty = true;\n scheduleRender();\n };\n\n const dispose = ((): void => {\n disposed = true;\n stopEffect();\n for (const row of rows.values()) {\n row.dispose();\n if (virtualize === undefined) row.el.remove();\n }\n rows.clear();\n renderSubscribers.clear();\n if (virtualize !== undefined) {\n parent.removeEventListener('scroll', scheduleRender);\n parentResize?.disconnect();\n container.remove(); // removes the inner sizer and its rows in one go\n VIRTUAL_INTERNALS.delete(handle);\n }\n }) as BindListHandle;\n const handle = dispose;\n handle.setHeight = setHeight;\n\n // Register the coordination surface the `observeRowHeights` helper needs, kept\n // off the public type (a GC-tied WeakMap, so it doesn't count against Design\n // rule 5). Only virtualized lists have a window to observe.\n if (virtualize !== undefined) {\n handle.container = container;\n VIRTUAL_INTERNALS.set(handle, {\n visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })),\n onRender: (cb) => {\n renderSubscribers.add(cb);\n return () => renderSubscribers.delete(cb);\n },\n });\n }\n\n return handle;\n}\n\n/** Internal coordination surface between {@link bindList} and {@link observeRowHeights}. */\ninterface VirtualInternals {\n /** The current visible rows, in order, with their keys. */\n visibleRows: () => Array<{ key: ListKey; el: HTMLElement }>;\n /** Subscribe to each window render; returns an unsubscribe. */\n onRender: (cb: () => void) => () => void;\n}\n\n// GC-tied (WeakMap) coordination store — a pure cache, not counted against\n// Design rule 5 (same class as `bindings.ts:insertedTextNodes`).\nconst VIRTUAL_INTERNALS = new WeakMap<object, VirtualInternals>();\n\n/**\n * Drive a **measured** virtualized `bindList` (`virtualize: { rowHeight: {\n * estimate } }`) from real layout: install ONE `ResizeObserver` over the visible\n * rows and forward each row's `offsetHeight` to `handle.setHeight`, re-observing\n * as the window shifts. Returns a disposer.\n *\n * This is the batteries-included measurement path; it is deliberately separate\n * from `bindList` (which never depends on `ResizeObserver`) — you can measure\n * however you like and call `handle.setHeight` yourself instead. A no-op for a\n * non-virtualized handle or where `ResizeObserver` is unavailable (SSR).\n *\n * const list = bindList(scrollEl, source, { key, render, virtualize: { rowHeight: { estimate: 64 } } });\n * const stopMeasuring = observeRowHeights(list);\n */\nexport function observeRowHeights(handle: BindListHandle): () => void {\n const internals = VIRTUAL_INTERNALS.get(handle);\n const RO = globalThis.ResizeObserver;\n if (internals === undefined || RO === undefined) return () => { /* nothing to observe */ };\n\n const keyByEl = new WeakMap<Element, ListKey>();\n const observer = new RO((entries) => {\n for (const entry of entries) {\n const k = keyByEl.get(entry.target);\n if (k !== undefined) handle.setHeight(k, (entry.target as HTMLElement).offsetHeight);\n }\n });\n\n const resync = (): void => {\n observer.disconnect();\n for (const { key: k, el } of internals.visibleRows()) {\n keyByEl.set(el, k);\n observer.observe(el);\n }\n };\n\n const unsubscribe = internals.onRender(resync);\n resync(); // observe the initial window\n\n return () => {\n observer.disconnect();\n unsubscribe();\n };\n}\n"]} | ||
| {"version":3,"sources":["../src/list.ts"],"names":["dispose"],"mappings":"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","file":"list.js","sourcesContent":["/**\n * `kerfjs/list` — `bindList`, a keyed list with a live per-row mount and\n * optional viewport virtualization.\n *\n * This is a DELIBERATE second list API, distinct from `each()`. It does two\n * things `each()` structurally cannot:\n * 1. **Per-row reactivity.** Every row is individually `mount()`ed, so a signal\n * the row's `render` reads updates just that row (fine-grained binding or a\n * one-row morph) without touching its siblings — no full-list pass.\n * 2. **Virtualization.** With `{ virtualize: { rowHeight } }` only the rows in\n * the scroll viewport are rendered; padding on the scroll container keeps\n * `scrollHeight` honest. `rowHeight` is a fixed `number` (O(1) windowing), a\n * `(item, index) => number` for **app-declared variable** heights (a prefix\n * sum + binary-search window), or `{ estimate }` for **measured** heights —\n * the app reports real heights via the returned handle's `setHeight` (or the\n * `observeRowHeights` helper) and kerf anchor-corrects `scrollTop`. See\n * `docs/17-list-virtualization.md`.\n *\n * `each()` stays the choice for item-owned-state lists rendered to HTML strings;\n * reach for `bindList` when you need surgical per-row updates or windowing.\n *\n * import { bindList } from 'kerfjs/list';\n *\n * const dispose = bindList(listEl, itemsSignal, {\n * key: (row) => row.id,\n * render: (row) => <span class={selected} data-id={row.id}>{row.label}</span>,\n * tag: 'li',\n * virtualize: { rowHeight: 32 },\n * });\n *\n * `render` reads signals for reactivity (external state like a `selectedId`, or\n * signals the item carries) — keep the item OBJECTS stable across renders and\n * drive structure (add/remove/move) through `itemsSignal`. A row whose item\n * object identity changes is rebuilt (same rule as `each()`'s memo). `bindList`\n * OWNS `parent`'s children by default (append/move to the end) — to share\n * `parent` with fixed trailing siblings (an \"add\" button, an indicator), pass\n * `before` so the rows end just before that node. It reads `itemsSignal.value`,\n * so a plain `signal<T[]>` or an `arraySignal<T>` both work.\n */\nimport { ARRAY_SIGNAL_BRAND, type ArrayPatch } from './array-signal.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\nimport { moveNode } from './utils/moveNode.js';\n\n/** A row's stable key. */\nexport type ListKey = string | number;\n\n/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */\nexport interface ListSource<T> {\n readonly value: readonly T[];\n}\n\n/**\n * A row built imperatively by `render`: return the row **element** itself (kerf\n * keys/moves/reuses it and owns nothing inside it), or `{ el, update?, dispose? }`\n * to also hand back an `update(item)` — called on the SAME element when the row's\n * key persists but its item changes — and a `dispose` that runs only when the row\n * is removed.\n */\nexport type RowElement<T> =\n | HTMLElement\n | { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n\n/**\n * The virtualization height model:\n * - **`number`** — every row is this fixed pixel height (O(1) windowing).\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index.\n * - **`{ estimate }`** — **measured** heights: kerf uses `estimate` for a row\n * until the app reports its real height through {@link BindListHandle.setHeight}\n * (or the `observeRowHeights` helper). See `docs/17-list-virtualization.md`.\n */\nexport type RowHeight<T> =\n | number\n | ((item: T, index: number) => number)\n | { estimate: number | ((item: T, index: number) => number) };\n\n/**\n * The value {@link bindList} returns: a disposer you call to tear the list down,\n * augmented with `setHeight` for the **measured** virtualization mode.\n */\nexport type BindListHandle = (() => void) & {\n /**\n * Report a row's real pixel height (measured after layout) for\n * `virtualize: { rowHeight: { estimate } }` lists. Keyed by the list `key`, so\n * a report survives reorders. kerf recomputes the window and, if the row sits\n * ABOVE the viewport, anchor-corrects `scrollTop` so content doesn't jump.\n * A no-op for fixed / declared-height lists and for unknown keys.\n */\n setHeight: (key: ListKey, height: number) => void;\n /**\n * The inner container element kerf creates to hold the rows in a **virtualized**\n * list (the \"sizer\"). `undefined` for a non-virtualized list (there the rows\n * live directly in `parent`). Use it to style, id, or otherwise reach the row\n * block without guessing at `parent.lastElementChild` — though `containerClass`\n * / `containerId` on `virtualize` set those declaratively.\n */\n container?: HTMLElement;\n};\n\n/** Options for {@link bindList}. */\nexport interface BindListOptions<T> {\n /** Stable per-row key. Rows are matched, moved, and reused by this. */\n key: (item: T) => ListKey;\n /**\n * Build a row. Two modes, chosen per call by what you return:\n * - **Content mode** (a `MountResult` — JSX / `SafeHtml`): kerf creates the\n * row element (`tag`) and `mount()`s your content inside it, so signals your\n * content reads drive per-row reactivity.\n * - **Element mode** (an `HTMLElement`, or `{ el, update?, dispose? }`): the\n * element you return IS the row, so you own its tag, class, `data-*`, and\n * listeners. kerf **keys/moves/reuses** it — the SAME element survives an\n * append/remove/reorder or a fresh item object at the same key. Refresh its\n * content by reading signals inside it, or by returning an `update(item)`\n * that kerf calls on the existing element when the item changes. `dispose`\n * runs only when the row is genuinely removed.\n */\n render: (item: T) => MountResult | RowElement<T>;\n /** Row element tag for **content mode**. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). Ignored in element mode. */\n tag?: string;\n /**\n * Keep the rows as a contiguous block that ENDS just before this node, instead\n * of at the very end of `parent`. Use it when `parent` also holds non-row\n * siblings that must stay put — a trailing \"add\" button, a sliding indicator:\n * `before: () => addButton`. The node (a function is re-read each reconcile, or\n * pass the node directly) must be a child of `parent`. Without it, bindList\n * assumes exclusive ownership and appends rows to the end. Ignored when\n * virtualized (the rows own bindList's inner sizer exclusively).\n */\n before?: Node | (() => Node | null);\n /**\n * Turn on viewport virtualization. `parent` must be a scroll container (your\n * CSS: a fixed height + `overflow: auto`). `overscan` (default 3) is how many\n * extra rows to render above and below the viewport.\n *\n * `rowHeight` (a {@link RowHeight}) is the height model:\n * - **`number`** — every row is this fixed pixel height. O(1) windowing, no\n * cumulative model built.\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index. kerf builds a prefix sum of the\n * heights (rebuilt when the source array changes, not per scroll frame) and\n * binary-searches it to find the visible window. Return a non-negative\n * number of pixels.\n * - **`{ estimate }`** — **measured** heights for rows whose height is only\n * known after layout. kerf sizes an unmeasured row by `estimate` (a number\n * or an `(item, index) => number`), and the app reports each row's real\n * height via {@link BindListHandle.setHeight} (or the `observeRowHeights`\n * helper). kerf anchor-corrects `scrollTop` when an above-viewport row is\n * remeasured, so content doesn't jump.\n *\n * `minRows` renders **every** row (no windowing, zero padding) while the list\n * is shorter than it, and windows only at or above it — the DOM structure (the\n * inner container) is the same either way, so the call site never branches. A\n * fully-rendered short list is friendlier to find-in-page, screen readers, and\n * DOM-count assertions, which only see rows actually in the DOM.\n *\n * `containerClass` / `containerId` are set on the inner container kerf creates\n * to hold the rows, so it's reachable from CSS and tests without guessing at\n * `parent.lastElementChild` (it's also on the handle as `handle.container`).\n *\n * kerf re-windows on `parent`'s `scroll` and, where `ResizeObserver` exists, on\n * `parent` resizing — so a list that mounts before layout (a hidden tab,\n * `clientHeight` 0) fills in once it's sized, and a resized container re-windows.\n *\n * `mode` (default `'window'`) picks the virtualization STRATEGY:\n * - **`'window'`** — the JS windowing above: only the visible rows are in the\n * DOM, bounded node count, works on every engine. Off-window rows are removed\n * (see the findability tradeoff below).\n * - **`'content-visibility'`** — **every** row stays in the DOM and kerf sets\n * `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on\n * each one, so a supporting engine (Chromium, Safari 18) skips the *layout /\n * paint* of off-screen rows while keeping them findable. `rowHeight` here is\n * used **only** as the `contain-intrinsic-size` placeholder (scrollbar\n * accuracy before a row is first rendered) — there is no windowing math, no\n * padding, no scroll listener, no anchor correction, and `setHeight` /\n * `observeRowHeights` are **no-ops** (the browser owns real measurement).\n * `minRows` is ignored (all rows already render). On an engine without\n * `content-visibility` the CSS is simply inert — all rows render, correct and\n * fully findable, only without the skip optimization. Choose this mode for\n * medium lists where find-in-page / a11y / anchor links matter more than the\n * node ceiling; keep `'window'` for very large (100k-row) lists.\n *\n * **Findability tradeoff (`'window'` mode).** Off-window rows are removed from\n * the DOM (not merely hidden), so with `mode: 'window'`: **find-in-page\n * (Cmd/Ctrl+F)**, **screen readers / the a11y tree**, and **anchor links /\n * `scrollIntoView`** only reach the visible window — a match, an announced row,\n * or a linked element that has been windowed out isn't in the DOM to find.\n * Convey the true total via ARIA (`aria-rowcount` / `aria-setsize`) if it\n * matters, and use a non-virtualized list — `minRows` above the list length, or\n * `mode: 'content-visibility'` — when full findability matters more than the DOM\n * node ceiling. See `docs/17-list-virtualization.md` §17.10 / §17.11.\n */\n virtualize?: {\n rowHeight: RowHeight<T>;\n overscan?: number;\n minRows?: number;\n containerClass?: string;\n containerId?: string;\n /**\n * Virtualization strategy. `'window'` (default) removes off-window rows from\n * the DOM; `'content-visibility'` keeps every row in the DOM and lets the\n * browser skip off-screen layout/paint (full find-in-page / a11y, at the cost\n * of an unbounded node count). See the option JSDoc above.\n */\n mode?: 'window' | 'content-visibility';\n };\n}\n\ninterface Row<T> {\n el: HTMLElement;\n item: T;\n dispose: () => void;\n /** True for element-mode rows (the caller owns the element — reuse it, don't rebuild on item change). */\n elementMode: boolean;\n /** Element mode only: refresh the existing element when the item changes at the same key. */\n update?: (item: T) => void;\n}\n\n/**\n * Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a\n * `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears\n * down every row mount, the scroll listener (if virtualized), and the source\n * subscription.\n */\nexport function bindList<T>(\n parent: HTMLElement,\n source: ListSource<T>,\n options: BindListOptions<T>,\n): BindListHandle {\n const { key, render, tag = 'div', virtualize, before } = options;\n const overscan = virtualize?.overscan ?? 3;\n const minRows = virtualize?.minRows;\n // Content-visibility mode (KF-525): keep every row in the DOM and let the\n // browser skip off-screen layout/paint via CSS, instead of windowing rows out.\n // Feature-detection is deliberately absent: the CSS is inert on an unsupporting\n // engine (all rows still render, still findable) — that IS the graceful degrade.\n const contentVisibility = virtualize?.mode === 'content-visibility';\n\n // The node the row block ends before — `before` (KF-496) when the list shares\n // `parent` with trailing siblings, else the end of the container. Never applies\n // when virtualized: the rows own bindList's inner sizer exclusively.\n const endAnchor = (): Node | null => {\n if (virtualize !== undefined || before === undefined) return null;\n return (typeof before === 'function' ? before() : before) ?? null;\n };\n\n const rows = new Map<ListKey, Row<T>>();\n // The current DOM order of rows, kept in step by both the keyed-diff and the\n // granular patch paths so index-based patches can address rows directly.\n const order: Array<Row<T>> = [];\n let items: readonly T[] = [];\n let disposed = false;\n let rafPending = false;\n let firstRender = true;\n\n // Granular fast path (KF-478): when the source is an `arraySignal` and the\n // list is NOT virtualized, apply its insert/remove/move/update patches\n // directly in O(patches) instead of diffing the whole snapshot. Virtualized\n // lists keep the keyed diff — their visible set is just the window (cheap),\n // and absolute-index patches don't compose with a shifting window. A plain\n // `signal<T[]>` has no patches, so it always uses the keyed diff.\n const patchSource = source as {\n [ARRAY_SIGNAL_BRAND]?: boolean;\n _consumePatches?: () => ArrayPatch<T>[];\n };\n const granularEligible = virtualize === undefined && patchSource[ARRAY_SIGNAL_BRAND] === true;\n\n // Virtualized lists put the windowing padding + rows on an INNER sizer, so the\n // padding never inflates the scroll container's clientHeight (padding counts\n // toward clientHeight). `parent` stays the clean scroll viewport; `container`\n // holds the rows. Non-virtualized lists render straight into `parent`.\n const container: HTMLElement = virtualize === undefined ? parent : document.createElement('div');\n if (virtualize !== undefined) {\n if (virtualize.containerClass !== undefined) container.className = virtualize.containerClass;\n if (virtualize.containerId !== undefined) container.id = virtualize.containerId;\n parent.appendChild(container);\n }\n\n const NOOP = (): void => { /* element-mode rows with no caller teardown */ };\n\n // Detect element mode from a render result: a raw `HTMLElement`, or a\n // `{ el, dispose? }` object. Everything else (SafeHtml / string / nullish) is\n // content mode. SafeHtml is an object but has no `el`, so it never matches.\n const asElementRow = (\n rendered: MountResult | RowElement<T>,\n ): { el: HTMLElement; dispose: () => void; update?: (item: T) => void } | null => {\n if (rendered instanceof HTMLElement) return { el: rendered, dispose: NOOP };\n if (\n rendered !== null\n && typeof rendered === 'object'\n && 'el' in rendered\n && (rendered as { el: unknown }).el instanceof HTMLElement\n ) {\n const r = rendered as { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n return { el: r.el, dispose: r.dispose ?? NOOP, update: r.update };\n }\n return null;\n };\n\n const makeRow = (item: T): Row<T> => {\n // One call decides the mode per row (so a list may mix element + content rows).\n const elementRow = asElementRow(render(item));\n if (elementRow !== null) {\n // Element mode: the returned element IS the row; the caller owns its\n // content + cleanup. bindList sizes it for the windowing math per render\n // (see `sizeVisibleRows`), not here, since a variable height depends on the\n // row's current index in the full list.\n return { el: elementRow.el, item, dispose: elementRow.dispose, elementMode: true, update: elementRow.update };\n }\n // Content mode: kerf creates the row element and mounts `render` inside it,\n // so the content is per-row reactive. (In content mode `render` runs once\n // more here for the mode probe than the mount itself needs — keep it a pure\n // projection, which bindList already requires.)\n const el = document.createElement(tag);\n // Content mode: `render` returns a MountResult here (element results were\n // handled above), so narrowing it for `mount` is sound.\n const dispose = mount(el, () => render(item) as MountResult);\n return { el, item, dispose, elementMode: false };\n };\n\n // A row whose KEY persists but whose item object changed. Content-mode rows are\n // rebuilt (their mount re-renders the fresh item); element-mode rows are REUSED\n // — the caller owns the element, so we keep it (preserving focus / scroll /\n // listeners) and refresh via the optional `update(item)`. Returns the row to\n // use at that key (a fresh one for content, the same one for element).\n const reconcileItem = (row: Row<T>, k: ListKey, item: T): Row<T> => {\n if (row.item === item) return row;\n if (row.elementMode) {\n row.item = item;\n row.update?.(item);\n return row;\n }\n row.dispose();\n row.el.remove();\n rows.delete(k);\n const fresh = makeRow(item);\n rows.set(k, fresh);\n return fresh;\n };\n\n // Reconcile the live rows to exactly `visible`, in order, keyed.\n const syncRows = (visible: readonly T[]): void => {\n const wanted = new Set<ListKey>();\n for (const item of visible) wanted.add(key(item));\n\n // Remove rows that are gone from the window.\n for (const [k, row] of rows) {\n if (!wanted.has(k)) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n }\n }\n\n // Create missing rows; reuse existing ones by key (element rows keep their\n // element across item changes; content rows rebuild on identity change).\n order.length = 0;\n for (const item of visible) {\n const k = key(item);\n const existing = rows.get(k);\n let row: Row<T>;\n if (existing !== undefined) {\n row = reconcileItem(existing, k, item);\n } else {\n row = makeRow(item);\n rows.set(k, row);\n }\n order.push(row);\n }\n\n // Reverse pass: move only rows that are out of position. `moveNode` keeps a\n // reordered (already-connected) row's live state via `moveBefore` where\n // supported; a brand-new row (parentNode !== container, not yet connected)\n // falls back to `insertBefore` via the guard.\n let ref: Node | null = endAnchor();\n for (let i = order.length - 1; i >= 0; i--) {\n const el = order[i].el;\n if (el.parentNode !== container || el.nextSibling !== ref) {\n moveNode(container, el, ref);\n }\n ref = el;\n }\n };\n\n // Apply arraySignal structural patches directly to `order` + the DOM, in\n // O(patches). Indices are always valid by construction: `order` reflects the\n // last-rendered state and the patches are exactly the delta from it (bindList\n // drains the queue every render, and `replace` is filtered out by the caller,\n // which snapshots instead). The `splice()`s mirror `arraySignal`'s own\n // `_items` mutations exactly.\n const applyPatches = (patches: readonly ArrayPatch<T>[]): void => {\n for (const patch of patches) {\n if (patch.type === 'insert') {\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order.splice(patch.index, 0, row);\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n } else if (patch.type === 'remove') {\n const [row] = order.splice(patch.index, 1);\n row.dispose();\n row.el.remove();\n rows.delete(key(row.item));\n } else if (patch.type === 'move') {\n const [row] = order.splice(patch.from, 1);\n order.splice(patch.to, 0, row);\n // Relocating an existing connected row → state-preserving move.\n moveNode(container, row.el, order[patch.to + 1]?.el ?? endAnchor());\n } else if (patch.type === 'update') {\n // An item whose OBJECT identity changed: content rows rebuild (their mount\n // re-renders the fresh item); element rows are REUSED — keep the caller's\n // element and refresh via update(), re-keying if the key changed. A\n // same-ref update needs nothing (the row's mount reacts to its signals).\n const current = order[patch.index];\n if (current.item !== patch.item) {\n if (current.elementMode) {\n const oldKey = key(current.item);\n const newKey = key(patch.item);\n current.item = patch.item;\n if (newKey !== oldKey) {\n rows.delete(oldKey);\n rows.set(newKey, current);\n }\n current.update?.(patch.item);\n } else {\n current.dispose();\n current.el.remove();\n rows.delete(key(current.item));\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order[patch.index] = row;\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n }\n }\n }\n // 'replace' never reaches here — the caller snapshots on it.\n }\n };\n\n // Virtualization height model, three modes:\n // - `fixedHeight` (a `number`): the O(1) fast path — no cumulative model.\n // - `variableHeightAt` (a function): app-declared per-row heights.\n // - measuring (`{ estimate }`): `variableHeightAt` returns the measured height\n // when the app has reported one (via `setHeight`), else the estimate.\n // In the two variable cases, `offsets[i]` is the total height of rows 0..i-1\n // (a prefix sum, length total+1), so `offsets[i+1] - offsets[i]` is row i's\n // height and `offsets[total]` is the full scroll height. It is rebuilt only\n // when `items` changes or a height is reported (heightsDirty), never per scroll\n // frame — a scroll reuses the prefix sum and pays only the O(log n) searches.\n const rowHeight = virtualize?.rowHeight;\n const fixedHeight = typeof rowHeight === 'number' ? rowHeight : null;\n const measuring = typeof rowHeight === 'object' && rowHeight !== null;\n const measured = new Map<ListKey, number>(); // key → real reported height\n const estimateAt = (index: number): number => {\n const est = (rowHeight as { estimate: number | ((item: T, index: number) => number) }).estimate;\n return typeof est === 'function' ? est(items[index], index) : est;\n };\n const variableHeightAt: ((index: number) => number) | null =\n fixedHeight !== null\n ? null\n : measuring\n ? (index): number => {\n const k = key(items[index]);\n return measured.has(k) ? (measured.get(k) as number) : estimateAt(index);\n }\n : (index): number => (rowHeight as (item: T, index: number) => number)(items[index], index);\n\n // Content-visibility mode only: the `contain-intrinsic-size` placeholder for\n // row `index`, derived from the same `rowHeight` source — a fixed `number`, a\n // declared `(item, index) => number`, or `{ estimate }` (its estimate; there is\n // no measurement in this mode, so `variableHeightAt`'s measured-height branch\n // never fires — `measured` stays empty).\n const intrinsicSizeAt = (index: number): number =>\n fixedHeight !== null ? fixedHeight : (variableHeightAt as (index: number) => number)(index);\n\n let offsets: number[] = [0];\n let heightsDirty = true;\n // Measuring only: key → current absolute index, so `setHeight(key, …)` locates\n // the row in O(1). Rebuilt with the prefix sum when `items` changes.\n const indexByKey = new Map<ListKey, number>();\n // Accumulated scroll-anchor correction: the summed height delta of remeasured\n // rows that sit entirely ABOVE the viewport top, applied to `scrollTop` before\n // the next window render so on-screen content does not jump.\n let pendingAnchorDelta = 0;\n\n const rebuildOffsets = (): void => {\n const fn = variableHeightAt as (index: number) => number;\n const total = items.length;\n offsets = new Array<number>(total + 1);\n offsets[0] = 0;\n if (measuring) indexByKey.clear();\n for (let i = 0; i < total; i++) {\n offsets[i + 1] = offsets[i] + fn(i);\n if (measuring) indexByKey.set(key(items[i]), i);\n }\n // Prune reported heights for keys no longer in the source, so a measured list\n // with key churn (a feed prepending new ids over a long session) doesn't grow\n // `measured` without bound. `indexByKey` now holds exactly the live keys (all\n // of them, windowed or not, since we walked every item). A key that only\n // scrolled out of the window stays — it's still in the source.\n if (measuring) {\n for (const k of measured.keys()) if (!indexByKey.has(k)) measured.delete(k);\n }\n };\n\n // Greatest index i in [0, total] with `offsets[i] <= target` — the first row\n // whose top is at or above `target` (the viewport top).\n const findStart = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (offsets[mid] <= target) lo = mid;\n else hi = mid - 1;\n }\n return lo;\n };\n\n // Smallest index i in [0, total] with `offsets[i] >= target` — one past the\n // last row that starts before `target` (the viewport bottom). `total` if none.\n const findEnd = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (offsets[mid] >= target) hi = mid;\n else lo = mid + 1;\n }\n return lo;\n };\n\n // Size each visible row for the windowing math. `order` holds the visible rows\n // in order, so `order[j]` is the item at absolute index `start + j`.\n // MEASURED mode is the exception: the row must take its NATURAL height so the\n // app (or `observeRowHeights`) can read the real `offsetHeight` — forcing a\n // height here would make the measurement echo the estimate. Its offsets come\n // from `setHeight` reports instead.\n const sizeVisibleRows = (start: number): void => {\n if (measuring) return;\n for (let j = 0; j < order.length; j++) {\n const abs = start + j;\n const h = fixedHeight !== null ? fixedHeight : offsets[abs + 1] - offsets[abs];\n order[j].el.style.height = `${h}px`;\n }\n };\n\n // Called after each virtualized window render (used by `observeRowHeights` to\n // re-observe the current visible rows).\n const renderSubscribers = new Set<() => void>();\n\n const renderWindow = (): void => {\n if (virtualize === undefined) {\n if (granularEligible) {\n // Always drain to keep the single patch queue clean (so patches never\n // double-apply). Take the granular path past the first render, when\n // there are patches, and none is a `replace` (which reshapes the whole\n // array — snapshot instead). Otherwise fall through to a keyed diff.\n const patches = patchSource._consumePatches!();\n if (\n !firstRender\n && patches.length > 0\n && !patches.some((p) => p.type === 'replace')\n ) {\n applyPatches(patches);\n return;\n }\n }\n syncRows(items);\n firstRender = false;\n return;\n }\n if (contentVisibility) {\n // Every row stays in the DOM (full find-in-page / a11y); the browser skips\n // off-screen layout/paint. No windowing, no padding, no scroll math — just\n // reconcile all rows and set the two CSS props (`start` is 0, so `order[j]`\n // is item j). On an unsupporting engine the CSS is inert but harmless.\n syncRows(items);\n for (let j = 0; j < order.length; j++) {\n const el = order[j].el;\n el.style.contentVisibility = 'auto';\n el.style.containIntrinsicSize = `0 ${intrinsicSizeAt(j)}px`;\n }\n return;\n }\n const total = items.length;\n let start: number;\n let end: number;\n let padTop: number;\n let padBottom: number;\n if (minRows !== undefined && total < minRows) {\n // Below the threshold: render EVERY row, no windowing, zero padding — one\n // DOM structure (the inner container) shared with the windowed path, so the\n // caller never branches. Rows are still sized (declared/fixed) from the\n // prefix sum, which we still build for the sizing pass.\n if (fixedHeight === null && heightsDirty) {\n rebuildOffsets();\n heightsDirty = false;\n }\n start = 0;\n end = total;\n padTop = 0;\n padBottom = 0;\n } else {\n const scrollTop = parent.scrollTop;\n const viewportBottom = scrollTop + parent.clientHeight;\n if (fixedHeight !== null) {\n start = Math.max(0, Math.floor(scrollTop / fixedHeight) - overscan);\n end = Math.min(total, Math.ceil(viewportBottom / fixedHeight) + overscan);\n padTop = start * fixedHeight;\n padBottom = Math.max(0, total - end) * fixedHeight;\n } else {\n if (heightsDirty) {\n rebuildOffsets();\n heightsDirty = false;\n }\n start = Math.max(0, findStart(scrollTop, total) - overscan);\n end = Math.min(total, findEnd(viewportBottom, total) + overscan);\n padTop = offsets[start];\n padBottom = offsets[total] - offsets[end];\n }\n }\n syncRows(items.slice(start, end));\n sizeVisibleRows(start);\n container.style.paddingTop = `${padTop}px`;\n container.style.paddingBottom = `${padBottom}px`;\n for (const cb of renderSubscribers) cb();\n };\n\n const stopEffect = effect(() => {\n items = source.value; // tracking read — re-runs on any structural change\n heightsDirty = true; // items changed → the prefix sum (if any) is stale\n renderWindow();\n });\n\n // One rAF-coalesced render, shared by scroll and by measurement reports. A\n // pending anchor correction is applied to `scrollTop` first (which itself may\n // fire a scroll, but with the delta already cleared the follow-up is a no-op).\n const scheduleRender = (): void => {\n if (rafPending) return;\n rafPending = true;\n globalThis.requestAnimationFrame(() => {\n rafPending = false;\n if (disposed) return;\n if (pendingAnchorDelta !== 0) {\n parent.scrollTop += pendingAnchorDelta;\n pendingAnchorDelta = 0;\n }\n renderWindow();\n });\n };\n // Content-visibility mode needs no scroll listener (there's no window to\n // recompute — the browser handles off-screen skipping itself).\n if (virtualize !== undefined && !contentVisibility) parent.addEventListener('scroll', scheduleRender);\n\n // Re-window when `parent` RESIZES, not just on scroll. This makes two cases\n // robust that the scroll-only model missed: a list mounted before layout\n // (`clientHeight` 0 — a hidden tab, pre-first-paint) fills in once it's sized,\n // and a container resized while open re-windows. ResizeObserver fires an\n // initial callback on observe, so the 0-height case self-heals with no synthetic\n // scroll. Absent (older SSR/runtime) → scroll-only, as before.\n const RO = globalThis.ResizeObserver;\n const parentResize =\n virtualize !== undefined && !contentVisibility && RO !== undefined ? new RO(scheduleRender) : undefined;\n parentResize?.observe(parent);\n\n // Measured mode: report a row's real height. No-op for fixed / declared lists\n // and for keys not currently in the list.\n const setHeight = (k: ListKey, height: number): void => {\n // No-op for fixed / declared lists, unknown keys, and content-visibility mode\n // (the browser owns measurement there — no windowing to correct).\n if (!measuring || contentVisibility) return;\n const idx = indexByKey.get(k);\n if (idx === undefined) return;\n const oldHeight = measured.has(k) ? (measured.get(k) as number) : estimateAt(idx);\n if (height === oldHeight) return;\n measured.set(k, height);\n // A row whose bottom is at/above the viewport top shifts everything below it\n // (the on-screen content) by the height delta — correct `scrollTop` to match.\n // Uses the CURRENT (pre-rebuild) offsets, which reflect the on-screen layout.\n if (offsets[idx + 1] <= parent.scrollTop) pendingAnchorDelta += height - oldHeight;\n heightsDirty = true;\n scheduleRender();\n };\n\n const dispose = ((): void => {\n disposed = true;\n stopEffect();\n for (const row of rows.values()) {\n row.dispose();\n if (virtualize === undefined) row.el.remove();\n }\n rows.clear();\n renderSubscribers.clear();\n if (virtualize !== undefined) {\n parent.removeEventListener('scroll', scheduleRender);\n parentResize?.disconnect();\n container.remove(); // removes the inner sizer and its rows in one go\n VIRTUAL_INTERNALS.delete(handle);\n }\n }) as BindListHandle;\n const handle = dispose;\n handle.setHeight = setHeight;\n\n // Register the coordination surface the `observeRowHeights` helper needs, kept\n // off the public type (a GC-tied WeakMap, so it doesn't count against Design\n // rule 5). Only virtualized lists have a window to observe.\n if (virtualize !== undefined) {\n handle.container = container;\n // Content-visibility mode registers no internals, so `observeRowHeights` is a\n // clean no-op on it (there's nothing to measure — the browser owns layout).\n if (!contentVisibility) {\n VIRTUAL_INTERNALS.set(handle, {\n visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })),\n onRender: (cb) => {\n renderSubscribers.add(cb);\n return () => renderSubscribers.delete(cb);\n },\n });\n }\n }\n\n return handle;\n}\n\n/** Internal coordination surface between {@link bindList} and {@link observeRowHeights}. */\ninterface VirtualInternals {\n /** The current visible rows, in order, with their keys. */\n visibleRows: () => Array<{ key: ListKey; el: HTMLElement }>;\n /** Subscribe to each window render; returns an unsubscribe. */\n onRender: (cb: () => void) => () => void;\n}\n\n// GC-tied (WeakMap) coordination store — a pure cache, not counted against\n// Design rule 5 (same class as `bindings.ts:insertedTextNodes`).\nconst VIRTUAL_INTERNALS = new WeakMap<object, VirtualInternals>();\n\n/**\n * Drive a **measured** virtualized `bindList` (`virtualize: { rowHeight: {\n * estimate } }`) from real layout: install ONE `ResizeObserver` over the visible\n * rows and forward each row's `offsetHeight` to `handle.setHeight`, re-observing\n * as the window shifts. Returns a disposer.\n *\n * This is the batteries-included measurement path; it is deliberately separate\n * from `bindList` (which never depends on `ResizeObserver`) — you can measure\n * however you like and call `handle.setHeight` yourself instead. A no-op for a\n * non-virtualized handle or where `ResizeObserver` is unavailable (SSR).\n *\n * const list = bindList(scrollEl, source, { key, render, virtualize: { rowHeight: { estimate: 64 } } });\n * const stopMeasuring = observeRowHeights(list);\n */\nexport function observeRowHeights(handle: BindListHandle): () => void {\n const internals = VIRTUAL_INTERNALS.get(handle);\n const RO = globalThis.ResizeObserver;\n if (internals === undefined || RO === undefined) return () => { /* nothing to observe */ };\n\n const keyByEl = new WeakMap<Element, ListKey>();\n const observer = new RO((entries) => {\n for (const entry of entries) {\n const k = keyByEl.get(entry.target);\n if (k !== undefined) handle.setHeight(k, (entry.target as HTMLElement).offsetHeight);\n }\n });\n\n const resync = (): void => {\n observer.disconnect();\n for (const { key: k, el } of internals.visibleRows()) {\n keyByEl.set(el, k);\n observer.observe(el);\n }\n };\n\n const unsubscribe = internals.onRender(resync);\n resync(); // observe the initial window\n\n return () => {\n observer.disconnect();\n unsubscribe();\n };\n}\n"]} |
+35
-0
@@ -148,2 +148,19 @@ import { SafeHtml } from './jsx-runtime.js'; | ||
| outsideIgnore?: Element | readonly Element[]; | ||
| /** | ||
| * Opt into the browser **top layer** (`docs/19-native-overlay-backing.md`). | ||
| * When `true` and the engine supports it, a modal overlay (`trap: true`) is | ||
| * hosted in a `<dialog>` opened with `.showModal()` — real inerting of the rest | ||
| * of the document + guaranteed stacking above any `z-index` — and a non-modal | ||
| * one (`trap: false`) uses the Popover API (`[popover]` + `showPopover()`). | ||
| * Feature-detected; falls back to today's plain `<div>` where unsupported. | ||
| * | ||
| * The `render` slot + promise API are unchanged — kerf just hosts your markup | ||
| * in a `<dialog>` / `[popover]` instead of a `<div>`. Two caveats: native | ||
| * `<dialog>` / `[popover]` carry **UA default styles** (a `::backdrop`, | ||
| * centering, border, padding) that kerf does not reset — style the element (and | ||
| * its `::backdrop`) via `className`; and `container` is effectively a **no-op** | ||
| * for visual position, since the top layer ignores where the element lives in | ||
| * the DOM. Default `false`. | ||
| */ | ||
| native?: boolean; | ||
| } | ||
@@ -192,2 +209,4 @@ /** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */ | ||
| danger?: boolean; | ||
| /** Host the dialog in the browser top layer (`<dialog>.showModal()`) where supported. See {@link OverlayOptions.native}. */ | ||
| native?: boolean; | ||
| /** | ||
@@ -234,2 +253,4 @@ * Bring your own markup (design-system dialogs): return the full dialog body, | ||
| validate?: FieldValidator; | ||
| /** Host the dialog in the browser top layer (`<dialog>.showModal()`) where supported. See {@link OverlayOptions.native}. */ | ||
| native?: boolean; | ||
| /** | ||
@@ -309,2 +330,4 @@ * Bring your own markup: return the full dialog body, spreading the provided | ||
| cancelText?: string; | ||
| /** Host the dialog in the browser top layer (`<dialog>.showModal()`) where supported. See {@link OverlayOptions.native}. */ | ||
| native?: boolean; | ||
| /** | ||
@@ -352,2 +375,4 @@ * Bring your own markup: return the full form body, laying out `slots.fields` | ||
| defaultValue?: R; | ||
| /** Host the dialog in the browser top layer (`<dialog>.showModal()`) where supported. See {@link OverlayOptions.native}. */ | ||
| native?: boolean; | ||
| /** Bring your own markup: return the full body, spreading each `slots.actions[i]` onto your buttons. */ | ||
@@ -390,2 +415,10 @@ render?: (slots: ChoiceRenderSlots) => OverlayContent; | ||
| onDismiss?: () => void; | ||
| /** | ||
| * Host the popover in the browser top layer (the Popover API — `[popover]` + | ||
| * `showPopover()`) where supported, so it stacks above any `z-index` without a | ||
| * z-index war. Falls back to today's plain `<div>` where unsupported. kerf keeps | ||
| * owning positioning + its own dismiss wiring; the popover is `popover="manual"`. | ||
| * See {@link OverlayOptions.native}. Default `false`. | ||
| */ | ||
| native?: boolean; | ||
| } | ||
@@ -416,2 +449,4 @@ /** | ||
| role?: string; | ||
| /** Host the tooltip in the browser top layer (the Popover API) where supported. See {@link OverlayOptions.native}. Default `false`. */ | ||
| native?: boolean; | ||
| } | ||
@@ -418,0 +453,0 @@ /** |
+63
-15
@@ -1,2 +0,2 @@ | ||
| import { mount } from './chunk-LKWAKC2X.js'; | ||
| import { mount } from './chunk-SRWQKB33.js'; | ||
| import { delegate } from './chunk-KEZTD6H4.js'; | ||
@@ -127,2 +127,8 @@ import './chunk-QIP723L4.js'; | ||
| } | ||
| function supportsDialog() { | ||
| return typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function"; | ||
| } | ||
| function supportsPopover() { | ||
| return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function"; | ||
| } | ||
| function overlay(content, options = {}) { | ||
@@ -137,9 +143,13 @@ const { | ||
| onDismiss, | ||
| outsideIgnore | ||
| outsideIgnore, | ||
| native = false | ||
| } = options; | ||
| const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss]; | ||
| const restoreTo = document.activeElement; | ||
| const wrapper = document.createElement("div"); | ||
| const useDialog = native && trap && supportsDialog(); | ||
| const usePopover = native && !trap && supportsPopover(); | ||
| const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div"); | ||
| wrapper.className = className; | ||
| if (trap) { | ||
| if (usePopover) wrapper.setAttribute("popover", "manual"); | ||
| if (trap && !useDialog) { | ||
| wrapper.setAttribute("role", role); | ||
@@ -150,2 +160,11 @@ wrapper.setAttribute("aria-modal", "true"); | ||
| const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content); | ||
| let nativeOpened = false; | ||
| if (useDialog) { | ||
| wrapper.showModal(); | ||
| nativeOpened = true; | ||
| } else if (usePopover) { | ||
| wrapper.showPopover(); | ||
| wrapper.style.inset = "auto"; | ||
| nativeOpened = true; | ||
| } | ||
| const removers = []; | ||
@@ -162,2 +181,7 @@ const resultBox = {}; | ||
| disposeMount(); | ||
| if (nativeOpened) { | ||
| nativeOpened = false; | ||
| if (useDialog) wrapper.close(); | ||
| else wrapper.hidePopover(); | ||
| } | ||
| wrapper.remove(); | ||
@@ -172,3 +196,10 @@ if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus(); | ||
| const wantEscape = triggers.includes("escape"); | ||
| if (wantEscape || trap) { | ||
| if (useDialog) { | ||
| const onCancel = (event) => { | ||
| event.preventDefault(); | ||
| if (wantEscape) userDismiss(); | ||
| }; | ||
| wrapper.addEventListener("cancel", onCancel); | ||
| removers.push(() => wrapper.removeEventListener("cancel", onCancel)); | ||
| } else if (wantEscape || trap) { | ||
| const onKeydown = (event) => { | ||
@@ -244,2 +275,3 @@ if (wantEscape && event.key === "Escape") { | ||
| danger = false, | ||
| native = false, | ||
| render | ||
@@ -271,3 +303,4 @@ } = options; | ||
| initialFocus: '[data-confirm="ok"]', | ||
| trap: true | ||
| trap: true, | ||
| native | ||
| }); | ||
@@ -290,2 +323,3 @@ delegate(handle.el, "click", "[data-confirm]", (_event, el) => { | ||
| validate, | ||
| native = false, | ||
| render | ||
@@ -331,3 +365,4 @@ } = options; | ||
| initialFocus: "[data-prompt-input]", | ||
| trap: true | ||
| trap: true, | ||
| native | ||
| }); | ||
@@ -363,3 +398,11 @@ const input = handle.el.querySelector("[data-prompt-input]"); | ||
| function form(fields, options = {}) { | ||
| const { container, className = "kerf-overlay", title, okText = "OK", cancelText = "Cancel", render } = options; | ||
| const { | ||
| container, | ||
| className = "kerf-overlay", | ||
| title, | ||
| okText = "OK", | ||
| cancelText = "Cancel", | ||
| native = false, | ||
| render | ||
| } = options; | ||
| const fieldAttrs = (field) => ({ | ||
@@ -414,3 +457,4 @@ "data-field": field.name, | ||
| initialFocus: "[data-field]", | ||
| trap: true | ||
| trap: true, | ||
| native | ||
| }); | ||
@@ -467,3 +511,3 @@ const byAttr = (attr, name) => Array.from(handle.el.querySelectorAll(`[${attr}]`)).find( | ||
| function choice(message, actions, options = {}) { | ||
| const { container, className = "kerf-overlay", title, defaultValue, render } = options; | ||
| const { container, className = "kerf-overlay", title, defaultValue, native = false, render } = options; | ||
| const hasDefault = "defaultValue" in options; | ||
@@ -498,3 +542,4 @@ const actionAttrs = actions.map((_, i) => ({ "data-choice": String(i) })); | ||
| initialFocus: "[data-choice]", | ||
| trap: true | ||
| trap: true, | ||
| native | ||
| }); | ||
@@ -527,3 +572,4 @@ delegate(handle.el, "click", "[data-choice]", (_event, el) => { | ||
| outsideIgnore, | ||
| onDismiss | ||
| onDismiss, | ||
| native = false | ||
| } = options; | ||
@@ -538,3 +584,4 @@ const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore]; | ||
| onDismiss, | ||
| outsideIgnore: [anchor, ...extraIgnore] | ||
| outsideIgnore: [anchor, ...extraIgnore], | ||
| native | ||
| }); | ||
@@ -554,3 +601,4 @@ const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap }); | ||
| align = "start", | ||
| gap = 4 | ||
| gap = 4, | ||
| native = false | ||
| } = options; | ||
@@ -561,3 +609,3 @@ const body = typeof content === "function" ? content : typeof content === "string" ? jsx("span", { class: `${className}__text`, children: content }) : content; | ||
| function show() { | ||
| const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false }); | ||
| const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false, native }); | ||
| handle.el.setAttribute("role", role); | ||
@@ -564,0 +612,0 @@ const stop = autoReposition(handle.el, anchor, { placement, align, gap }); |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { mount } from './chunk-LKWAKC2X.js'; | ||
| import { mount } from './chunk-SRWQKB33.js'; | ||
| import './chunk-QIP723L4.js'; | ||
@@ -3,0 +3,0 @@ import './chunk-YHH7OUFA.js'; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { mount } from './chunk-LKWAKC2X.js'; | ||
| import { mount } from './chunk-SRWQKB33.js'; | ||
| import { delegate } from './chunk-KEZTD6H4.js'; | ||
@@ -3,0 +3,0 @@ import './chunk-QIP723L4.js'; |
+1
-0
@@ -40,2 +40,3 @@ # kerf | ||
| - [List virtualization](https://github.com/brianwestphal/kerf/blob/main/docs/17-list-virtualization.md): `bindList`'s virtualization height models — fixed `number`, app-declared `(item, index) => number`, and measured `{ estimate }` + `setHeight` — with kerf owning the cumulative-offset math and scroll anchoring while the app owns measurement (the `observeRowHeights` helper), plus the `minRows` render-all threshold and the container/resize ergonomics. | ||
| - [State-preserving moves](https://github.com/brianwestphal/kerf/blob/main/docs/18-state-preserving-moves.md): connected-row reorders (every `each()` / `bindList` / `morph` move site) use `Node.prototype.moveBefore()` where the engine supports it — an atomic move that keeps focus, selection, `<iframe>` state, playing media, and running CSS animations across the reorder — falling back to `insertBefore()` otherwise. Transparent internal `moveNode` helper; no API change. | ||
@@ -42,0 +43,0 @@ ## Examples |
+1
-1
| { | ||
| "name": "kerfjs", | ||
| "version": "4.2.0", | ||
| "version": "4.3.0-beta.1", | ||
| "description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
| import { itemVersion } from './chunk-QIP723L4.js'; | ||
| import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js'; | ||
| import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-SUPUPSBE.js'; | ||
| import { effect } from './chunk-3APBEVHF.js'; | ||
| import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js'; | ||
| import { devHooks } from './chunk-VVDJLWMP.js'; | ||
| // src/list-render-state.ts | ||
| function deriveListRenderState(bindingCount) { | ||
| if (bindingCount === void 0) return "unbound"; | ||
| return bindingCount === 0 ? "empty" : "bound"; | ||
| } | ||
| function decideListPath(state, patches, snapshotLength, previousBindingCount) { | ||
| if (state === "unbound") return { path: "snapshot", reason: "first-render" }; | ||
| if (state === "empty") return { path: "snapshot", reason: "empty-binding" }; | ||
| if (patches.length === 0) return { path: "snapshot", reason: "no-patches" }; | ||
| let netDelta = 0; | ||
| for (const p of patches) { | ||
| if (p.type === "insert") netDelta += 1; | ||
| else if (p.type === "remove") netDelta -= 1; | ||
| else if (p.type === "replace") return { path: "snapshot", reason: "replace" }; | ||
| } | ||
| const count = previousBindingCount ?? 0; | ||
| if (count + netDelta !== snapshotLength) { | ||
| return { path: "snapshot", reason: "count-drift" }; | ||
| } | ||
| return { path: "granular" }; | ||
| } | ||
| // src/each.ts | ||
| var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal"); | ||
| function isArraySignal(value) { | ||
| return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true; | ||
| } | ||
| var context = null; | ||
| var renderingRow = false; | ||
| function inRowScope(fn) { | ||
| const prev = renderingRow; | ||
| renderingRow = true; | ||
| try { | ||
| return fn(); | ||
| } finally { | ||
| renderingRow = prev; | ||
| } | ||
| } | ||
| function _setRenderContext(c) { | ||
| context = c; | ||
| } | ||
| function _resetCallOrderListState(ctx) { | ||
| const isCallOrderId = (id) => !id.startsWith("k:"); | ||
| for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) { | ||
| for (const id of Array.from(map.keys())) { | ||
| if (isCallOrderId(id)) map.delete(id); | ||
| } | ||
| } | ||
| } | ||
| function isEachOptions(v) { | ||
| return typeof v === "object" && v !== null; | ||
| } | ||
| var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/; | ||
| function assertValidKey(key) { | ||
| if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) { | ||
| throw new Error( | ||
| `each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.` | ||
| ); | ||
| } | ||
| } | ||
| function claimKey(ctx, key) { | ||
| assertValidKey(key); | ||
| if (renderingRow) { | ||
| throw new Error( | ||
| `each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.` | ||
| ); | ||
| } | ||
| if (ctx.keysThisRender.has(key)) { | ||
| throw new Error( | ||
| `each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.` | ||
| ); | ||
| } | ||
| ctx.keysThisRender.add(key); | ||
| return `k:${key}`; | ||
| } | ||
| function each(items, render, cacheKeyOrOptions) { | ||
| const useOptions = isEachOptions(cacheKeyOrOptions); | ||
| const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions; | ||
| const listKey = useOptions ? cacheKeyOrOptions.key : void 0; | ||
| if (isArraySignal(items) && context !== null) { | ||
| return eachGranular(items, render, cacheKey, listKey); | ||
| } | ||
| const snapshotItems = isArraySignal(items) ? items.value : items; | ||
| return eachSnapshot(snapshotItems, render, cacheKey, listKey); | ||
| } | ||
| function eachSnapshot(items, render, cacheKey, listKey) { | ||
| let id; | ||
| if (context !== null) { | ||
| id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++); | ||
| } else { | ||
| id = "orphan"; | ||
| } | ||
| return eachSnapshotById(items, render, cacheKey, id); | ||
| } | ||
| function assertObjectItem(item, index) { | ||
| if (typeof item !== "object" || item === null) { | ||
| throw new Error( | ||
| `each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).` | ||
| ); | ||
| } | ||
| } | ||
| function eachGranular(sig, render, cacheKey, listKey) { | ||
| const ctx = context; | ||
| const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++); | ||
| const previousBindingCount = ctx.bindingCounts.get(id); | ||
| const patches = sig._consumePatches(); | ||
| const snapshot = sig.value; | ||
| const previousSource = ctx.bindingSources.get(id); | ||
| const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig; | ||
| if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id); | ||
| const decision = sourceReused ? { path: "snapshot" } : decideListPath( | ||
| deriveListRenderState(previousBindingCount), | ||
| patches, | ||
| snapshot.length, | ||
| previousBindingCount | ||
| ); | ||
| if (decision.path === "snapshot") { | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| let staleIndexShift = false; | ||
| if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) { | ||
| const rendered = []; | ||
| for (let i = 0; i < previousBindingCount; i++) rendered.push(i); | ||
| for (const p of patches) { | ||
| if (p.type === "insert") rendered.splice(p.index, 0, p.index); | ||
| else if (p.type === "remove") rendered.splice(p.index, 1); | ||
| else if (p.type === "move") { | ||
| const [moved] = rendered.splice(p.from, 1); | ||
| rendered.splice(p.to, 0, moved); | ||
| } | ||
| } | ||
| for (let i = 0; i < rendered.length; i++) { | ||
| if (rendered[i] !== i) { | ||
| staleIndexShift = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (cacheKey !== void 0) { | ||
| const cache2 = ctx.caches.get(id); | ||
| for (let i = 0; i < snapshot.length; i++) { | ||
| const item = snapshot[i]; | ||
| const k = cacheKey(item, i); | ||
| const cached = cache2.get(item); | ||
| if (cached !== void 0 && cached.cacheKey !== k) { | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| } | ||
| } | ||
| const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => { | ||
| const out = render(item, index); | ||
| return isSafeHtml(out) ? out.toString() : out; | ||
| })); | ||
| const internalPatches = new Array(patches.length); | ||
| const cache = ctx.caches.get(id); | ||
| try { | ||
| for (let i = 0; i < patches.length; i++) { | ||
| const p = patches[i]; | ||
| if (p.type === "insert" || p.type === "update") { | ||
| assertObjectItem(p.item, p.index); | ||
| const { html, bindings } = renderRow(p.item, p.index); | ||
| internalPatches[i] = { | ||
| type: p.type, | ||
| index: p.index, | ||
| item: p.item, | ||
| html, | ||
| bindings | ||
| }; | ||
| cache?.set(p.item, { | ||
| cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0, | ||
| html, | ||
| bindings, | ||
| version: itemVersion(p.item), | ||
| index: p.index | ||
| }); | ||
| } else { | ||
| internalPatches[i] = p; | ||
| } | ||
| } | ||
| } catch { | ||
| ctx.bindingCounts.delete(id); | ||
| return eachSnapshotById(snapshot, render, cacheKey, id, sig); | ||
| } | ||
| if (staleIndexShift) devHooks.staleIndex?.(id); | ||
| return granularListSafeHtml(id, [], internalPatches, sig); | ||
| } | ||
| function eachSnapshotById(items, render, cacheKey, id, source) { | ||
| let cache = null; | ||
| if (context !== null) { | ||
| let c = context.caches.get(id); | ||
| if (c === void 0) { | ||
| c = /* @__PURE__ */ new WeakMap(); | ||
| context.caches.set(id, c); | ||
| } | ||
| cache = c; | ||
| } | ||
| const segItems = new Array(items.length); | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (let i = 0; i < items.length; i++) { | ||
| const item = items[i]; | ||
| assertObjectItem(item, i); | ||
| if (seen.has(item)) { | ||
| throw new Error( | ||
| `each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).` | ||
| ); | ||
| } | ||
| seen.add(item); | ||
| const k = cacheKey ? cacheKey(item, i) : void 0; | ||
| const version = itemVersion(item); | ||
| let html; | ||
| let bindings; | ||
| const cached = cache !== null ? cache.get(item) : void 0; | ||
| if (cached !== void 0 && cached.cacheKey === k && cached.version === version) { | ||
| html = cached.html; | ||
| bindings = cached.bindings; | ||
| if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) { | ||
| devHooks.staleIndex?.(id); | ||
| } | ||
| } else { | ||
| const captured = captureRowBindings(() => inRowScope(() => { | ||
| const out = render(item, i); | ||
| return isSafeHtml(out) ? out.toString() : out; | ||
| })); | ||
| html = captured.html; | ||
| bindings = captured.bindings; | ||
| if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i }); | ||
| } | ||
| segItems[i] = { ref: item, cacheKey: k, html, bindings }; | ||
| } | ||
| if (cacheKey !== void 0) { | ||
| devHooks.duplicateCacheKeys?.(id, segItems); | ||
| } | ||
| return listSafeHtml(id, segItems, source); | ||
| } | ||
| // src/list-reconcile-focus.ts | ||
| function captureFocus(liveParent) { | ||
| const active = document.activeElement; | ||
| if (active === null || active === document.body) return null; | ||
| if (!liveParent.contains(active)) return null; | ||
| const el = active; | ||
| let selStart = null; | ||
| let selEnd = null; | ||
| if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") { | ||
| try { | ||
| selStart = el.selectionStart; | ||
| selEnd = el.selectionEnd; | ||
| } catch { | ||
| } | ||
| } | ||
| return { el, selStart, selEnd }; | ||
| } | ||
| function restoreFocus(snap) { | ||
| if (document.activeElement === snap.el) return; | ||
| if (!snap.el.isConnected) return; | ||
| snap.el.focus(); | ||
| if (snap.selStart !== null && snap.selEnd !== null) { | ||
| try { | ||
| snap.el.setSelectionRange(snap.selStart, snap.selEnd); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| // src/morph.ts | ||
| var ID_KEY_PREFIX = "id:"; | ||
| var DATA_KEY_PREFIX = "data-key:"; | ||
| var ELEMENT_NODE = 1; | ||
| var TEXT_NODE = 3; | ||
| var COMMENT_NODE = 8; | ||
| function getNodeKey(node) { | ||
| if (node.nodeType !== ELEMENT_NODE) return void 0; | ||
| const el = node; | ||
| if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`; | ||
| if (el.dataset !== void 0 && el.dataset.key !== void 0) { | ||
| return `${DATA_KEY_PREFIX}${el.dataset.key}`; | ||
| } | ||
| return void 0; | ||
| } | ||
| var EMPTY_OWNED = /* @__PURE__ */ new Set(); | ||
| function morph(liveRoot, template, ownedItems = EMPTY_OWNED) { | ||
| if (liveRoot == null) { | ||
| throw new Error( | ||
| '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.' | ||
| ); | ||
| } | ||
| const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template); | ||
| const focusSnap = captureFocus(liveRoot); | ||
| morphChildren(liveRoot, templateEl, ownedItems); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| } | ||
| function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) { | ||
| morphElement(fromEl, toEl, ownedItems); | ||
| } | ||
| function isElementNode(t) { | ||
| return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE; | ||
| } | ||
| function parseTemplate(liveRoot, template) { | ||
| const el = liveRoot.cloneNode(false); | ||
| el.innerHTML = String(template); | ||
| return el; | ||
| } | ||
| function protectionTag(node) { | ||
| const { dataset } = node; | ||
| return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : ""); | ||
| } | ||
| var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX]; | ||
| function isMarker(node) { | ||
| if (node.nodeType !== COMMENT_NODE) return false; | ||
| const { data } = node; | ||
| return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix)); | ||
| } | ||
| function markersPairable(a, b) { | ||
| if (!isMarker(a) && !isMarker(b)) return true; | ||
| return a.data === b.data; | ||
| } | ||
| function skipOwned(node, ownedItems) { | ||
| while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) { | ||
| node = node.nextSibling; | ||
| } | ||
| return node; | ||
| } | ||
| function isListMarker(node) { | ||
| return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX); | ||
| } | ||
| function afterListRegion(marker, ownedItems) { | ||
| let last = marker; | ||
| for (let r = marker.nextSibling; r !== null; r = r.nextSibling) { | ||
| if (isListMarker(r)) break; | ||
| if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r; | ||
| } | ||
| return last.nextSibling; | ||
| } | ||
| function morphChildren(fromParent, toParent, ownedItems) { | ||
| const keyed = /* @__PURE__ */ new Map(); | ||
| for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) { | ||
| if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue; | ||
| const k = getNodeKey(c); | ||
| if (k !== void 0) keyed.set(k, c); | ||
| } | ||
| let fromChild = skipOwned(fromParent.firstChild, ownedItems); | ||
| let toChild = toParent.firstChild; | ||
| while (toChild !== null) { | ||
| const toNext = toChild.nextSibling; | ||
| let matched = null; | ||
| const toKey = getNodeKey(toChild); | ||
| if (toKey !== void 0 && keyed.has(toKey)) { | ||
| matched = keyed.get(toKey); | ||
| keyed.delete(toKey); | ||
| if (matched !== fromChild) { | ||
| fromParent.insertBefore(matched, fromChild); | ||
| } else { | ||
| fromChild = skipOwned(fromChild.nextSibling, ownedItems); | ||
| } | ||
| } | ||
| if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) { | ||
| matched = fromChild; | ||
| fromChild = skipOwned( | ||
| isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling, | ||
| ownedItems | ||
| ); | ||
| if (matched.nodeType === COMMENT_NODE && fromChild !== null) { | ||
| const owned = boundTextNodeOf(matched); | ||
| if (owned !== null && fromChild === owned) { | ||
| fromChild = skipOwned(owned.nextSibling, ownedItems); | ||
| } | ||
| } | ||
| } | ||
| if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) { | ||
| const toTag = toChild.tagName; | ||
| for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) { | ||
| if (scan.nodeType !== ELEMENT_NODE) continue; | ||
| const el = scan; | ||
| if (ownedItems.has(el)) continue; | ||
| if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue; | ||
| if (protectionTag(el) !== protectionTag(toChild)) continue; | ||
| matched = el; | ||
| fromParent.insertBefore(el, fromChild); | ||
| break; | ||
| } | ||
| } | ||
| if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) { | ||
| const wantData = toChild.data; | ||
| for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) { | ||
| if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue; | ||
| const regionEnd = afterListRegion(scan, ownedItems); | ||
| const run = []; | ||
| for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) { | ||
| run.push(r); | ||
| } | ||
| const focusSnap = captureFocus(fromParent); | ||
| for (const node of run) fromParent.insertBefore(node, fromChild); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| matched = scan; | ||
| break; | ||
| } | ||
| } | ||
| if (matched !== null) { | ||
| morphNode(matched, toChild, ownedItems); | ||
| } else { | ||
| const cloned = toChild.cloneNode(true); | ||
| fromParent.insertBefore(cloned, fromChild); | ||
| } | ||
| toChild = toNext; | ||
| } | ||
| while (fromChild !== null) { | ||
| const next = fromChild.nextSibling; | ||
| if (fromChild.nodeType === ELEMENT_NODE) { | ||
| const el = fromChild; | ||
| if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) { | ||
| fromParent.removeChild(fromChild); | ||
| } | ||
| } else { | ||
| fromParent.removeChild(fromChild); | ||
| } | ||
| fromChild = next; | ||
| } | ||
| } | ||
| function morphNode(fromNode, toNode, ownedItems) { | ||
| if (fromNode.nodeType === ELEMENT_NODE) { | ||
| morphElement(fromNode, toNode, ownedItems); | ||
| return; | ||
| } | ||
| if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) { | ||
| const fromText = fromNode; | ||
| const toText = toNode; | ||
| if (fromText.data !== toText.data) fromText.data = toText.data; | ||
| } | ||
| } | ||
| function morphElement(fromEl, toEl, ownedItems) { | ||
| if (fromEl.tagName !== toEl.tagName) { | ||
| const replacement = toEl.cloneNode(true); | ||
| fromEl.parentNode?.replaceChild(replacement, fromEl); | ||
| return; | ||
| } | ||
| if (fromEl.dataset.morphSkip !== void 0) return; | ||
| if (fromEl.isEqualNode(toEl)) return; | ||
| if (fromEl === document.activeElement) { | ||
| const ce = fromEl.getAttribute("contenteditable"); | ||
| if (ce !== null && ce.toLowerCase() !== "false") return; | ||
| if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl); | ||
| } | ||
| morphAttributes(fromEl, toEl); | ||
| if (fromEl.dataset.morphSkipChildren !== void 0) return; | ||
| const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent; | ||
| morphChildren(fromEl, toEl, ownedItems); | ||
| if (syncTextareaValue) { | ||
| fromEl.value = toEl.textContent; | ||
| } | ||
| } | ||
| function isUserAgentOwnedAttr(tagName, name) { | ||
| return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG"); | ||
| } | ||
| function morphAttributes(fromEl, toEl) { | ||
| const toAttrs = toEl.attributes; | ||
| for (let i = 0; i < toAttrs.length; i++) { | ||
| const attr = toAttrs[i]; | ||
| const ns = attr.namespaceURI; | ||
| const name = attr.localName; | ||
| const value = attr.value; | ||
| if (ns !== null) { | ||
| if (fromEl.getAttributeNS(ns, name) !== value) { | ||
| fromEl.setAttributeNS(ns, attr.name, value); | ||
| } | ||
| } else if (fromEl.getAttribute(name) !== value) { | ||
| fromEl.setAttribute(name, value); | ||
| syncFormProp(fromEl, name, value, true); | ||
| } | ||
| } | ||
| const fromAttrs = fromEl.attributes; | ||
| const fromTag = fromEl.tagName; | ||
| for (let i = fromAttrs.length - 1; i >= 0; i--) { | ||
| const attr = fromAttrs[i]; | ||
| const ns = attr.namespaceURI; | ||
| const name = attr.localName; | ||
| if (ns !== null) { | ||
| if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name); | ||
| } else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) { | ||
| fromEl.removeAttribute(name); | ||
| syncFormProp(fromEl, name, "", false); | ||
| } | ||
| } | ||
| } | ||
| function isTextInputOrTextarea(el) { | ||
| if (el.tagName === "TEXTAREA") return true; | ||
| if (el.tagName === "INPUT") { | ||
| const type = el.type; | ||
| return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === ""; | ||
| } | ||
| return false; | ||
| } | ||
| function preserveTextEntryState(fromEl, toEl) { | ||
| if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") { | ||
| const fromInput = fromEl; | ||
| const toInput = toEl; | ||
| toInput.value = fromInput.value; | ||
| try { | ||
| toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| // src/list-binding.ts | ||
| function endAnchor(binding) { | ||
| if (binding.items.length > 0) { | ||
| return binding.items[binding.items.length - 1].node.nextSibling; | ||
| } | ||
| return binding.marker.nextSibling; | ||
| } | ||
| // src/list-reconcile-fast-paths.ts | ||
| var LT = 60; | ||
| var GT = 62; | ||
| var DQUOTE = 34; | ||
| var SQUOTE = 39; | ||
| var AMP = 38; | ||
| var EQ = 61; | ||
| var SLASH = 47; | ||
| var TEXT_NODE2 = 3; | ||
| var ELEMENT_NODE2 = 1; | ||
| function isWhitespace(cc) { | ||
| return cc === 32 || cc === 9 || cc === 10 || cc === 13; | ||
| } | ||
| function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) { | ||
| const oldGt = oldHtml.indexOf(">"); | ||
| const newGt = newHtml.indexOf(">"); | ||
| if (oldGt === -1 || newGt === -1) return false; | ||
| if (oldHtml.length - oldGt !== newHtml.length - newGt) return false; | ||
| if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false; | ||
| if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false; | ||
| const oldTag = parseOpeningTag(oldHtml, oldGt); | ||
| const newTag = parseOpeningTag(newHtml, newGt); | ||
| if (oldTag === null || newTag === null) return false; | ||
| if (oldTag.tagName !== newTag.tagName) return false; | ||
| for (const name of oldTag.attrs.keys()) { | ||
| if (name.indexOf(":") !== -1) return false; | ||
| } | ||
| for (const name of newTag.attrs.keys()) { | ||
| if (name.indexOf(":") !== -1) return false; | ||
| } | ||
| const liveTagUpper = liveNode.tagName; | ||
| for (const [name, rawValue] of newTag.attrs) { | ||
| const oldValue = oldTag.attrs.get(name); | ||
| if (oldValue === rawValue) continue; | ||
| const value = unescapeAttrValue(rawValue); | ||
| liveNode.setAttribute(name, value); | ||
| syncFormProp(liveNode, name, value, true); | ||
| } | ||
| for (const name of oldTag.attrs.keys()) { | ||
| if (newTag.attrs.has(name)) continue; | ||
| if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue; | ||
| liveNode.removeAttribute(name); | ||
| syncFormProp(liveNode, name, "", false); | ||
| } | ||
| return true; | ||
| } | ||
| function tryTextContentFastPath(liveNode, oldHtml, newHtml) { | ||
| if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false; | ||
| let p = 0; | ||
| const minLen = Math.min(oldHtml.length, newHtml.length); | ||
| while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++; | ||
| let s = 0; | ||
| const maxS = minLen - p; | ||
| while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) { | ||
| s++; | ||
| } | ||
| const oldWinEnd = oldHtml.length - s; | ||
| const newWinEnd = newHtml.length - s; | ||
| if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false; | ||
| if (!isPureTextWindow(newHtml, p, newWinEnd)) return false; | ||
| if (p === 0) return false; | ||
| const boundaryCc = oldHtml.charCodeAt(p - 1); | ||
| if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false; | ||
| const textStart = lastIndexOfChar(oldHtml, GT, p - 1); | ||
| if (textStart === -1) return false; | ||
| const textEnd = oldHtml.indexOf("<", p); | ||
| if (textEnd === -1) return false; | ||
| if (textEnd < oldWinEnd) return false; | ||
| const newTextEnd = textEnd + (newHtml.length - oldHtml.length); | ||
| const oldText = oldHtml.slice(textStart + 1, textEnd); | ||
| const newText = newHtml.slice(textStart + 1, newTextEnd); | ||
| if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false; | ||
| const textIdx = countTextNodesBefore(oldHtml, textStart + 1); | ||
| const targetNode = nthTextNodeDescendant(liveNode, textIdx); | ||
| if (targetNode === null) return false; | ||
| if (targetNode.nodeValue !== oldText) return false; | ||
| targetNode.nodeValue = newText; | ||
| const host = targetNode.parentNode; | ||
| if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) { | ||
| host.value = newText; | ||
| } | ||
| return true; | ||
| } | ||
| function containsDataMorphSkip(html) { | ||
| return html.indexOf("data-morph-skip") !== -1; | ||
| } | ||
| function isPureTextWindow(html, start, end) { | ||
| for (let i = start; i < end; i++) { | ||
| const cc = html.charCodeAt(i); | ||
| if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function lastIndexOfChar(html, target, beforeInclusive) { | ||
| for (let i = beforeInclusive; i >= 0; i--) { | ||
| if (html.charCodeAt(i) === target) return i; | ||
| } | ||
| return -1; | ||
| } | ||
| function countTextNodesBefore(html, beforePos) { | ||
| let count = 0; | ||
| let i = 0; | ||
| while (i < beforePos) { | ||
| if (html.charCodeAt(i) === LT) { | ||
| while (i < beforePos && html.charCodeAt(i) !== GT) i++; | ||
| i++; | ||
| } else { | ||
| const start = i; | ||
| while (i < beforePos && html.charCodeAt(i) !== LT) i++; | ||
| if (i > start) count++; | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
| function nthTextNodeDescendant(root, n) { | ||
| let count = 0; | ||
| let result = null; | ||
| function walk(node) { | ||
| for (let c = node.firstChild; c !== null; c = c.nextSibling) { | ||
| if (result !== null) return; | ||
| if (c.nodeType === TEXT_NODE2) { | ||
| if (count === n) { | ||
| result = c; | ||
| return; | ||
| } | ||
| count++; | ||
| } else if (c.nodeType === ELEMENT_NODE2) { | ||
| walk(c); | ||
| } | ||
| } | ||
| } | ||
| walk(root); | ||
| return result; | ||
| } | ||
| function parseOpeningTag(html, gtPos) { | ||
| if (html.charCodeAt(0) !== LT) return null; | ||
| let i = 1; | ||
| let end = gtPos; | ||
| if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1; | ||
| const nameStart = i; | ||
| while (i < end) { | ||
| const cc = html.charCodeAt(i); | ||
| if (isWhitespace(cc)) break; | ||
| i++; | ||
| } | ||
| const tagName = html.slice(nameStart, i); | ||
| if (tagName.length === 0) return null; | ||
| const attrs = /* @__PURE__ */ new Map(); | ||
| while (i < end) { | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i >= end) break; | ||
| const aNameStart = i; | ||
| while (i < end) { | ||
| const cc = html.charCodeAt(i); | ||
| if (cc === EQ || isWhitespace(cc)) break; | ||
| i++; | ||
| } | ||
| const aName = html.slice(aNameStart, i); | ||
| if (aName.length === 0) return null; | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i < end && html.charCodeAt(i) === EQ) { | ||
| i++; | ||
| while (i < end && isWhitespace(html.charCodeAt(i))) i++; | ||
| if (i >= end) return null; | ||
| const q = html.charCodeAt(i); | ||
| if (q !== DQUOTE && q !== SQUOTE) return null; | ||
| i++; | ||
| const vStart = i; | ||
| while (i < end && html.charCodeAt(i) !== q) i++; | ||
| if (i >= end) return null; | ||
| attrs.set(aName, html.slice(vStart, i)); | ||
| i++; | ||
| } else { | ||
| attrs.set(aName, ""); | ||
| } | ||
| } | ||
| return { tagName, attrs }; | ||
| } | ||
| function unescapeAttrValue(s) { | ||
| if (s.indexOf("&") === -1) return s; | ||
| return s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"); | ||
| } | ||
| function isUserAgentOwnedAttr2(tagNameUpper, name) { | ||
| return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG"); | ||
| } | ||
| // src/list-reconcile-granular.ts | ||
| function reconcileGranular(binding, patches) { | ||
| const { liveParent } = binding; | ||
| const items = binding.items; | ||
| const focusSnap = captureFocus(liveParent); | ||
| let i = 0; | ||
| while (i < patches.length) { | ||
| const patch = patches[i]; | ||
| if (patch.type === "replace") { | ||
| i += 1; | ||
| continue; | ||
| } | ||
| if (patch.type === "update") { | ||
| let runEnd = i + 1; | ||
| while (runEnd < patches.length && patches[runEnd].type === "update") { | ||
| runEnd += 1; | ||
| } | ||
| const runLen = runEnd - i; | ||
| if (runLen === 1) { | ||
| applySingleUpdate(liveParent, items, patch); | ||
| } else { | ||
| applyBulkUpdate(liveParent, items, patches, i, runEnd); | ||
| } | ||
| i = runEnd; | ||
| continue; | ||
| } | ||
| if (patch.type === "insert") { | ||
| let runEnd = i + 1; | ||
| while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) { | ||
| runEnd += 1; | ||
| } | ||
| const runLen = runEnd - i; | ||
| if (runLen === 1) { | ||
| applySingleInsert(liveParent, items, patch, endAnchor(binding)); | ||
| } else { | ||
| applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding)); | ||
| } | ||
| i = runEnd; | ||
| continue; | ||
| } | ||
| if (patch.type === "remove") { | ||
| const entry = items[patch.index]; | ||
| disposeRowBindings(entry.bindingDisposers); | ||
| liveParent.removeChild(entry.node); | ||
| items.splice(patch.index, 1); | ||
| i += 1; | ||
| continue; | ||
| } | ||
| if (patch.type === "move") { | ||
| const moved = items[patch.from]; | ||
| let anchorIdx = patch.to; | ||
| if (patch.from < patch.to) anchorIdx += 1; | ||
| const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding); | ||
| liveParent.insertBefore(moved.node, anchor); | ||
| items.splice(patch.from, 1); | ||
| items.splice(patch.to, 0, moved); | ||
| i += 1; | ||
| continue; | ||
| } | ||
| } | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| if (items.length > 0) { | ||
| devHooks.missingRowKey?.(items[0].node, items[0].html, binding); | ||
| } | ||
| } | ||
| function applySingleInsert(liveParent, items, patch, tailAnchor) { | ||
| const { html } = patch; | ||
| const newNode = parseSingleRow(html, patch.index, liveParent); | ||
| const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor; | ||
| liveParent.insertBefore(newNode, anchor); | ||
| items.splice(patch.index, 0, { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: newNode, | ||
| bindings: patch.bindings, | ||
| // KF-294: wire the inserted row's fine-grained bindings to its new node. | ||
| bindingDisposers: wireRowIfBound(newNode, patch.bindings) | ||
| }); | ||
| } | ||
| function wireRowIfBound(node, bindings) { | ||
| return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0; | ||
| } | ||
| function applySingleUpdate(liveParent, items, patch) { | ||
| const { html } = patch; | ||
| const oldEntry = items[patch.index]; | ||
| if (html === oldEntry.html) { | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| return; | ||
| } | ||
| if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) { | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| return; | ||
| } | ||
| const newNode = parseSingleRow(html, patch.index, liveParent); | ||
| applyParsedRowUpdate(liveParent, items, patch, html, newNode); | ||
| } | ||
| function applyParsedRowUpdate(liveParent, items, patch, html, newNode) { | ||
| const oldEntry = items[patch.index]; | ||
| if (oldEntry.node.tagName === newNode.tagName) { | ||
| _morphElement(oldEntry.node, newNode); | ||
| items[patch.index] = reuseBound(patch, html, oldEntry); | ||
| } else { | ||
| disposeRowBindings(oldEntry.bindingDisposers); | ||
| liveParent.replaceChild(newNode, oldEntry.node); | ||
| items[patch.index] = { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: newNode, | ||
| bindings: patch.bindings, | ||
| bindingDisposers: wireRowIfBound(newNode, patch.bindings) | ||
| }; | ||
| } | ||
| } | ||
| function reuseBound(patch, html, oldEntry) { | ||
| const kept = carryOrRewireRowBindings( | ||
| oldEntry.node, | ||
| oldEntry.bindings, | ||
| oldEntry.bindingDisposers, | ||
| patch.bindings | ||
| ); | ||
| return { | ||
| ref: patch.item, | ||
| cacheKey: void 0, | ||
| html, | ||
| node: oldEntry.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| function applyBulkUpdate(liveParent, items, patches, start, end) { | ||
| const morphChanges = []; | ||
| for (let k = start; k < end; k++) { | ||
| const p = patches[k]; | ||
| const oldEntry = items[p.index]; | ||
| if (p.html === oldEntry.html) { | ||
| items[p.index] = reuseBound(p, p.html, oldEntry); | ||
| continue; | ||
| } | ||
| if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) { | ||
| items[p.index] = reuseBound(p, p.html, oldEntry); | ||
| continue; | ||
| } | ||
| morphChanges.push({ patchIdx: k, html: p.html }); | ||
| } | ||
| if (morphChanges.length === 0) return; | ||
| const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent); | ||
| if (count !== morphChanges.length) { | ||
| throw findOffendingChange(patches, morphChanges, liveParent); | ||
| } | ||
| const newNodes = collectTemplateChildren(content, morphChanges.length); | ||
| for (let k = 0; k < morphChanges.length; k++) { | ||
| const c = morphChanges[k]; | ||
| const p = patches[c.patchIdx]; | ||
| applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]); | ||
| } | ||
| } | ||
| function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) { | ||
| const startIdx = patches[start].index; | ||
| const htmls = new Array(end - start); | ||
| for (let k = start; k < end; k++) { | ||
| htmls[k - start] = patches[k].html; | ||
| } | ||
| const { content, count } = parseRowTemplate(htmls.join(""), liveParent); | ||
| if (count !== htmls.length) { | ||
| throw findOffendingInsert(patches, start, htmls, liveParent); | ||
| } | ||
| const newNodes = collectTemplateChildren(content, end - start); | ||
| const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor; | ||
| liveParent.insertBefore(content, anchor); | ||
| const newEntries = new Array(end - start); | ||
| for (let k = 0; k < newEntries.length; k++) { | ||
| const p = patches[start + k]; | ||
| newEntries[k] = { | ||
| ref: p.item, | ||
| cacheKey: void 0, | ||
| html: htmls[k], | ||
| node: newNodes[k], | ||
| bindings: p.bindings, | ||
| bindingDisposers: wireRowIfBound(newNodes[k], p.bindings) | ||
| // KF-294 | ||
| }; | ||
| } | ||
| items.splice(startIdx, 0, ...newEntries); | ||
| } | ||
| function findOffendingInsert(patches, start, htmls, liveParent) { | ||
| for (let i = 0; i < htmls.length; i++) { | ||
| if (parseRowTemplate(htmls[i], liveParent).count !== 1) { | ||
| return rowContractError(patches[start + i].index, htmls[i], liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| function findOffendingChange(patches, changes, liveParent) { | ||
| for (const c of changes) { | ||
| if (parseRowTemplate(c.html, liveParent).count !== 1) { | ||
| return rowContractError(patches[c.patchIdx].index, c.html, liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| // src/list-reconcile-inplace.ts | ||
| function tryInPlaceContentUpdate(binding, listSeg) { | ||
| const oldItems = binding.items; | ||
| const items = listSeg.items; | ||
| const n = items.length; | ||
| if (n === 0 || n !== oldItems.length) return false; | ||
| for (let i = 0; i < n; i++) { | ||
| if (items[i].ref !== oldItems[i].ref) return false; | ||
| } | ||
| const { liveParent } = binding; | ||
| const newRecord = new Array(n); | ||
| const focusSnap = captureFocus(liveParent); | ||
| for (let i = 0; i < n; i++) { | ||
| newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i); | ||
| } | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| binding.items = newRecord; | ||
| devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding); | ||
| return true; | ||
| } | ||
| function updateRowInPlace(liveParent, old, ni, index) { | ||
| if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) { | ||
| const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: old.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| const newNode = parseSingleRow(ni.html, index, liveParent); | ||
| if (old.node.tagName === newNode.tagName) { | ||
| _morphElement(old.node, newNode); | ||
| const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: old.node, | ||
| bindings: kept.bindings, | ||
| bindingDisposers: kept.bindingDisposers | ||
| }; | ||
| } | ||
| disposeRowBindings(old.bindingDisposers); | ||
| liveParent.replaceChild(newNode, old.node); | ||
| const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings); | ||
| return { | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: newNode, | ||
| bindings: fresh.bindings, | ||
| bindingDisposers: fresh.bindingDisposers | ||
| }; | ||
| } | ||
| // src/list-reconcile-snapshot.ts | ||
| function reconcileSnapshot(binding, listSeg) { | ||
| if (tryInPlaceContentUpdate(binding, listSeg)) return; | ||
| const { liveParent } = binding; | ||
| const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg); | ||
| const tailAnchor = endAnchor(binding); | ||
| buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent); | ||
| const focusSnap = captureFocus(liveParent); | ||
| removeOldNodes(liveParent, removedItems); | ||
| applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor); | ||
| if (focusSnap !== null) restoreFocus(focusSnap); | ||
| binding.items = newRecord; | ||
| if (newRecord.length > 0) { | ||
| devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding); | ||
| } | ||
| } | ||
| function classifyItems(oldItems, listSeg) { | ||
| const oldByRef = /* @__PURE__ */ new Map(); | ||
| for (let i = 0; i < oldItems.length; i++) { | ||
| oldByRef.set(oldItems[i].ref, [oldItems[i], i]); | ||
| } | ||
| const newRecord = new Array(listSeg.items.length); | ||
| const prevIdx = new Array(listSeg.items.length); | ||
| const removedItems = []; | ||
| const freshIndices = []; | ||
| const freshHtmls = []; | ||
| for (let i = 0; i < listSeg.items.length; i++) { | ||
| const ni = listSeg.items[i]; | ||
| const oi = oldByRef.get(ni.ref); | ||
| if (oi !== void 0) { | ||
| oldByRef.delete(ni.ref); | ||
| if (oi[0].html === ni.html) { | ||
| newRecord[i] = oi[0]; | ||
| prevIdx[i] = oi[1]; | ||
| continue; | ||
| } | ||
| removedItems.push(oi[0]); | ||
| } | ||
| newRecord[i] = { | ||
| // `node` placeholder is filled by `buildFreshNodes`; its parse-count | ||
| // check guarantees every fresh index gets a real element before use. | ||
| ref: ni.ref, | ||
| cacheKey: ni.cacheKey, | ||
| html: ni.html, | ||
| node: null, | ||
| bindings: ni.bindings | ||
| }; | ||
| prevIdx[i] = -1; | ||
| freshIndices.push(i); | ||
| freshHtmls.push(ni.html); | ||
| } | ||
| for (const [, orphan] of oldByRef) removedItems.push(orphan[0]); | ||
| return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls }; | ||
| } | ||
| function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) { | ||
| if (freshHtmls.length === 0) return; | ||
| const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent); | ||
| if (count !== freshHtmls.length) { | ||
| throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent); | ||
| } | ||
| let node = content.firstElementChild; | ||
| for (const idx of freshIndices) { | ||
| const next = node.nextElementSibling; | ||
| const item = newRecord[idx]; | ||
| item.node = node; | ||
| if (item.bindings !== void 0 && item.bindings.length > 0) { | ||
| item.bindingDisposers = wireRowBindings(item.node, item.bindings); | ||
| } | ||
| node = next; | ||
| } | ||
| } | ||
| function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) { | ||
| for (let i = 0; i < freshHtmls.length; i++) { | ||
| if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) { | ||
| return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent); | ||
| } | ||
| } | ||
| return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug)."); | ||
| } | ||
| function removeOldNodes(liveParent, removedItems) { | ||
| for (const item of removedItems) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (item.node.parentElement === liveParent) liveParent.removeChild(item.node); | ||
| } | ||
| } | ||
| function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) { | ||
| let nextSibling = tailAnchor; | ||
| for (let i = newRecord.length - 1; i >= 0; i--) { | ||
| const node = newRecord[i].node; | ||
| if (prevIdx[i] === -1 || !stable.has(i)) { | ||
| liveParent.insertBefore(node, nextSibling); | ||
| } | ||
| nextSibling = node; | ||
| } | ||
| } | ||
| function lis(arr) { | ||
| const tails = []; | ||
| const tailIdx = []; | ||
| const prev = new Array(arr.length); | ||
| for (let i = 0; i < arr.length; i++) { | ||
| const v = arr[i]; | ||
| if (v === -1) { | ||
| prev[i] = -1; | ||
| continue; | ||
| } | ||
| let lo = 0; | ||
| let hi = tails.length; | ||
| while (lo < hi) { | ||
| const mid = lo + hi >> 1; | ||
| if (tails[mid] < v) lo = mid + 1; | ||
| else hi = mid; | ||
| } | ||
| prev[i] = lo > 0 ? tailIdx[lo - 1] : -1; | ||
| tails[lo] = v; | ||
| tailIdx[lo] = i; | ||
| } | ||
| const out = /* @__PURE__ */ new Set(); | ||
| let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1; | ||
| while (k !== -1) { | ||
| out.add(k); | ||
| k = prev[k]; | ||
| } | ||
| return out; | ||
| } | ||
| // src/list-reconcile.ts | ||
| function reconcileList(binding, listSeg) { | ||
| if (listSeg.patches !== void 0 && binding.items.length > 0) { | ||
| reconcileGranular(binding, listSeg.patches); | ||
| return; | ||
| } | ||
| reconcileSnapshot(binding, listSeg); | ||
| } | ||
| // src/mount.ts | ||
| var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted"); | ||
| 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."; | ||
| function isMounted(el) { | ||
| return el[MOUNTED_MARKER] === true; | ||
| } | ||
| function setMounted(el, on) { | ||
| if (on) { | ||
| el[MOUNTED_MARKER] = true; | ||
| } else { | ||
| delete el[MOUNTED_MARKER]; | ||
| } | ||
| } | ||
| function describeEl(el) { | ||
| const tag = el.tagName.toLowerCase(); | ||
| const id = el.id ? `#${el.id}` : ""; | ||
| return `<${tag}${id}>`; | ||
| } | ||
| function assertNotInsideMountedTree(rootEl) { | ||
| if (isMounted(rootEl)) { | ||
| throw new Error( | ||
| `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.` | ||
| ); | ||
| } | ||
| let ancestor = rootEl.parentElement; | ||
| while (ancestor !== null) { | ||
| if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG); | ||
| ancestor = ancestor.parentElement; | ||
| } | ||
| const stack = []; | ||
| for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]); | ||
| while (stack.length > 0) { | ||
| const cur = stack.pop(); | ||
| if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG); | ||
| for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]); | ||
| } | ||
| } | ||
| function mount(rootEl, render) { | ||
| if (rootEl == null) { | ||
| throw new Error( | ||
| '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.' | ||
| ); | ||
| } | ||
| const owner = rootEl.ownerDocument; | ||
| if (owner !== document) { | ||
| if (owner.defaultView === null) document.adoptNode(rootEl); | ||
| } | ||
| assertNotInsideMountedTree(rootEl); | ||
| setMounted(rootEl, true); | ||
| const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null; | ||
| const bindings = /* @__PURE__ */ new Map(); | ||
| const renderCtx = { | ||
| counter: 0, | ||
| caches: /* @__PURE__ */ new Map(), | ||
| bindingCounts: /* @__PURE__ */ new Map(), | ||
| bindingSources: /* @__PURE__ */ new Map(), | ||
| keysThisRender: /* @__PURE__ */ new Set(), | ||
| shiftCandidates: [], | ||
| warnedShiftIds: /* @__PURE__ */ new Set(), | ||
| rebuiltLists: /* @__PURE__ */ new Set() | ||
| }; | ||
| const bindingCtx = newBindingContext(); | ||
| let bindingDisposers = []; | ||
| let prevWiredBindings = []; | ||
| let isFirst = true; | ||
| let prevStaticHtml = ""; | ||
| const valueOnlyWarnCtx = { warned: false }; | ||
| const runRenderPass = () => { | ||
| renderCtx.counter = 0; | ||
| renderCtx.keysThisRender.clear(); | ||
| renderCtx.shiftCandidates.length = 0; | ||
| bindingCtx.counter = 0; | ||
| bindingCtx.list = []; | ||
| _setRenderContext(renderCtx); | ||
| _setBindingContext(bindingCtx); | ||
| try { | ||
| return render(); | ||
| } finally { | ||
| _setRenderContext(null); | ||
| _setBindingContext(null); | ||
| } | ||
| }; | ||
| const disposeEffect = effect(() => { | ||
| let result = runRenderPass(); | ||
| const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter; | ||
| if (countChanged) { | ||
| for (const id of renderCtx.shiftCandidates) { | ||
| if (renderCtx.warnedShiftIds.has(id)) continue; | ||
| renderCtx.warnedShiftIds.add(id); | ||
| devHooks.listIdShift?.(id); | ||
| } | ||
| _resetCallOrderListState(renderCtx); | ||
| result = runRenderPass(); | ||
| } | ||
| let segment = resultToSegment(result); | ||
| if (isFirst) { | ||
| runFirstRender(rootEl, segment, bindings); | ||
| prevStaticHtml = flattenWithoutListItems(segment); | ||
| devHooks.parserRepair?.(prevStaticHtml); | ||
| bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers); | ||
| if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list; | ||
| isFirst = false; | ||
| } else { | ||
| let nextStaticHtml = runSubsequentRender( | ||
| rootEl, | ||
| segment, | ||
| bindings, | ||
| renderCtx, | ||
| prevStaticHtml, | ||
| valueOnlyWarnCtx | ||
| ); | ||
| if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) { | ||
| for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id); | ||
| result = runRenderPass(); | ||
| segment = resultToSegment(result); | ||
| nextStaticHtml = runSubsequentRender( | ||
| rootEl, | ||
| segment, | ||
| bindings, | ||
| renderCtx, | ||
| prevStaticHtml, | ||
| valueOnlyWarnCtx | ||
| ); | ||
| } | ||
| if (nextStaticHtml !== prevStaticHtml) { | ||
| bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers); | ||
| if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list; | ||
| } else { | ||
| devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list); | ||
| } | ||
| prevStaticHtml = nextStaticHtml; | ||
| } | ||
| const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null; | ||
| for (const listSeg of collectLists(segment).values()) { | ||
| const binding = bindings.get(listSeg.id); | ||
| if (binding === void 0) { | ||
| throw new Error( | ||
| "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." | ||
| ); | ||
| } | ||
| reconcileList(binding, listSeg); | ||
| renderCtx.bindingCounts.set(listSeg.id, binding.items.length); | ||
| renderCtx.bindingSources.set(listSeg.id, listSeg.source); | ||
| expectedCounts?.set( | ||
| listSeg.id, | ||
| listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length | ||
| ); | ||
| } | ||
| renderCtx.previousCallCount = renderCtx.counter; | ||
| devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0); | ||
| }); | ||
| return () => { | ||
| disposeEffect(); | ||
| for (const d of bindingDisposers) d(); | ||
| bindingDisposers = []; | ||
| for (const b of bindings.values()) { | ||
| for (const item of b.items) disposeRowBindings(item.bindingDisposers); | ||
| } | ||
| listenerWarnObserver?.disconnect(); | ||
| setMounted(rootEl, false); | ||
| }; | ||
| } | ||
| function runFirstRender(rootEl, segment, bindings) { | ||
| rootEl.innerHTML = flatten(segment, true); | ||
| bindListsFromMarkers(rootEl, segment, bindings, true); | ||
| } | ||
| function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) { | ||
| renderCtx.rebuiltLists.clear(); | ||
| const currentStaticHtml = flattenWithoutListItems(segment); | ||
| if (currentStaticHtml === prevStaticHtml) { | ||
| return prevStaticHtml; | ||
| } | ||
| devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx); | ||
| cleanupOrphanBindings(segment, bindings, renderCtx); | ||
| const template = rootEl.cloneNode(false); | ||
| template.innerHTML = currentStaticHtml; | ||
| morph(rootEl, template, collectOwnedItems(bindings)); | ||
| bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists); | ||
| return currentStaticHtml; | ||
| } | ||
| function coerceRenderResult(result) { | ||
| if (result === null || result === void 0) return ""; | ||
| if (result === false || result === true) return ""; | ||
| return String(result); | ||
| } | ||
| function resultToSegment(result) { | ||
| return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) }; | ||
| } | ||
| function anyRebuiltListIsGranular(segment, rebuilt) { | ||
| if (rebuilt.size === 0) return false; | ||
| const lists = collectLists(segment); | ||
| for (const id of rebuilt) { | ||
| if (lists.get(id)?.patches !== void 0) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) { | ||
| const lists = collectLists(segment); | ||
| const found = []; | ||
| collectComments(rootEl, found); | ||
| for (const marker of found) { | ||
| if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue; | ||
| const id = marker.data.slice(LIST_MARKER_PREFIX.length); | ||
| const existing = bindings.get(id); | ||
| if (existing !== void 0) { | ||
| if (existing.marker === marker && rootEl.contains(existing.marker)) continue; | ||
| for (const item of existing.items) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (rootEl.contains(item.node)) { | ||
| item.node.parentElement?.removeChild(item.node); | ||
| } | ||
| } | ||
| bindings.delete(id); | ||
| rebuiltLists?.add(id); | ||
| devHooks.listRebind?.(id, marker.parentElement); | ||
| } | ||
| const listSeg = lists.get(id); | ||
| const liveParent = marker.parentElement; | ||
| const items = []; | ||
| if (inlinedItems) { | ||
| let next = marker.nextElementSibling; | ||
| for (let i = 0; i < listSeg.items.length && next !== null; i++) { | ||
| validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent); | ||
| const rowBindings = listSeg.items[i].bindings; | ||
| const bound = { | ||
| ref: listSeg.items[i].ref, | ||
| cacheKey: listSeg.items[i].cacheKey, | ||
| html: listSeg.items[i].html, | ||
| node: next, | ||
| bindings: rowBindings | ||
| }; | ||
| if (rowBindings !== void 0 && rowBindings.length > 0) { | ||
| bound.bindingDisposers = wireRowBindings(next, rowBindings); | ||
| } | ||
| items.push(bound); | ||
| next = next.nextElementSibling; | ||
| } | ||
| } | ||
| const binding = { liveParent, items, marker }; | ||
| if (items.length > 0) { | ||
| devHooks.missingRowKey?.(items[0].node, items[0].html, binding); | ||
| } | ||
| devHooks.eachInMorphSkip?.(id, liveParent, rootEl); | ||
| bindings.set(id, binding); | ||
| } | ||
| } | ||
| function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) { | ||
| if (boundEl.outerHTML === expectedHtml) return; | ||
| const { content, count } = parseRowTemplate(expectedHtml, liveParent); | ||
| if (count !== 1) throw rowContractError(index, expectedHtml, liveParent); | ||
| const expectedTag = content.firstElementChild.tagName; | ||
| if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag); | ||
| } | ||
| function rowStructureError(index, gotTag, wantTag) { | ||
| const got = gotTag.toLowerCase(); | ||
| const want = wantTag.toLowerCase(); | ||
| return new Error( | ||
| `each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.` | ||
| ); | ||
| } | ||
| function collectOwnedItems(bindings) { | ||
| const owned = /* @__PURE__ */ new Set(); | ||
| for (const b of bindings.values()) { | ||
| for (const item of b.items) owned.add(item.node); | ||
| } | ||
| return owned; | ||
| } | ||
| function cleanupOrphanBindings(segment, bindings, renderCtx) { | ||
| const liveIds = collectLists(segment); | ||
| for (const [id, binding] of bindings) { | ||
| if (liveIds.has(id)) continue; | ||
| for (const item of binding.items) { | ||
| disposeRowBindings(item.bindingDisposers); | ||
| if (item.node.parentElement !== null) { | ||
| item.node.parentElement.removeChild(item.node); | ||
| } | ||
| } | ||
| if (binding.marker.parentElement !== null) { | ||
| binding.marker.parentElement.removeChild(binding.marker); | ||
| } | ||
| bindings.delete(id); | ||
| renderCtx.bindingCounts.delete(id); | ||
| renderCtx.bindingSources.delete(id); | ||
| renderCtx.caches.delete(id); | ||
| } | ||
| } | ||
| function collectComments(node, out) { | ||
| for (let c = node.firstChild; c !== null; c = c.nextSibling) { | ||
| if (c.nodeType === Node.COMMENT_NODE) out.push(c); | ||
| else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out); | ||
| } | ||
| } | ||
| export { each, morph, mount }; | ||
| //# sourceMappingURL=chunk-LKWAKC2X.js.map | ||
| //# sourceMappingURL=chunk-LKWAKC2X.js.map |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
1139472
2.59%7869
1.81%1
Infinity%