@kudzujs/core 0.6.20 → 0.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/framework/build.mjs +45 -7
- package/framework/core.d.ts +1 -1
- package/framework/core.mjs +48 -16
- package/framework/list-runtime.js +161 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -388,7 +388,18 @@ return <ItemList items={items} />
|
|
|
388
388
|
|
|
389
389
|
The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; no component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX, in one top-level immutable `const` rendered once as a JSX child, or in one synchronous wrapper receiving the state identifier as a direct prop. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with state as `[version, item.name]`, reruns only rows whose selected value changed; the replacement setup receives the complete latest item. Unrelated fields and reorder do not rerun it, while a key change removes and mounts the row. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
One nested keyed map may read a direct array property of its parent item. This supports category/item data populated after mount while preserving both parent and child DOM identity across updates and reorder:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
{categories.map(category => <section key={category.id}>
|
|
395
|
+
<h2>{category.title}</h2>
|
|
396
|
+
<ul>{category.items.map(item => <li key={item.id}>{item.title}</li>)}</ul>
|
|
397
|
+
</section>)}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The nested collection must be `parent.<field>`, the child row must have one intrinsic root, and child handlers may capture the child item. A second child list, third nesting level, computed collection, parent-item capture from the child row, child conditions, child effects, child components, and child row-local state remain unsupported.
|
|
401
|
+
|
|
402
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Same-file wrappers must be unexported and state-backed at every call; relative default, named/aliased, and direct named re-export wrappers are specialized per qualifying call. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a row may be empty, direct primitive Kudzu state identifiers, or direct `item.<field>` properties whose selected values remain JSON-safe primitives. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` dependencies are rejected. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package, namespace, and star-export list wrappers, package or namespace row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, nested item conditions beyond the direct one-level form above, component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
392
403
|
|
|
393
404
|
The focused wrapper fixture emits 1,393 B raw / 500 B gzip HTML and 10,719 B raw / 4,665 B gzip JavaScript across its route capabilities. After one warm-up, seven clean builds measured 314.1, 325.3, 322.3, 327.2, 336.1, 322.4, and 315.0 ms, with a 322.4 ms median.
|
|
394
405
|
|
package/framework/build.mjs
CHANGED
|
@@ -197,9 +197,10 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
197
197
|
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
|
|
198
198
|
const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
|
|
199
199
|
const hasListRowStates = plans.some(plan => plan.lists.some(list => list.rowStates))
|
|
200
|
+
const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
|
|
200
201
|
const hasItemDependencies = plans.some(plan => plan.effects.some(effect => effect.itemDependencies?.length))
|
|
201
202
|
const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
|
|
202
|
-
const hasListMounts = hasListConditions || plans.some(plan => plan.lists.some(list => list.mount))
|
|
203
|
+
const hasListMounts = hasListConditions || hasNestedLists || plans.some(plan => plan.lists.some(list => list.mount))
|
|
203
204
|
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
204
205
|
const hasSetterCaptures = hasCaptureType(plans, "setter")
|
|
205
206
|
const hasEffectCaptures = plans.some(plan => plan.effects.some(effect => Object.keys(effect.scope).length))
|
|
@@ -278,7 +279,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
278
279
|
__KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
|
|
279
280
|
__KUDZU_LIST_MOUNTS__: String(hasListMounts),
|
|
280
281
|
__KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
|
|
281
|
-
__KUDZU_LIST_ROW_STATES__: String(hasListRowStates)
|
|
282
|
+
__KUDZU_LIST_ROW_STATES__: String(hasListRowStates),
|
|
283
|
+
__KUDZU_NESTED_LISTS__: String(hasNestedLists)
|
|
282
284
|
})
|
|
283
285
|
}
|
|
284
286
|
if (hasNativeHandlers) {
|
|
@@ -1666,6 +1668,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1666
1668
|
const listValues = new WeakMap()
|
|
1667
1669
|
const listEventItems = new WeakMap()
|
|
1668
1670
|
const listConditions = new WeakMap()
|
|
1671
|
+
const nestedLists = new WeakMap()
|
|
1669
1672
|
const listEffectEntries = new WeakMap()
|
|
1670
1673
|
let usesBehavior = false
|
|
1671
1674
|
let usesBinding = false
|
|
@@ -2018,7 +2021,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2018
2021
|
calculation.parent = callback
|
|
2019
2022
|
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
2020
2023
|
}
|
|
2021
|
-
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState)
|
|
2024
|
+
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists)
|
|
2022
2025
|
if (specialization?.effects.length) {
|
|
2023
2026
|
usesListEffects = true
|
|
2024
2027
|
const statements = specialization.effects.map(entry => {
|
|
@@ -2185,14 +2188,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2185
2188
|
}
|
|
2186
2189
|
|
|
2187
2190
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2188
|
-
const
|
|
2191
|
+
const nestedParts = nestedLists.get(unwrapExpression(node.expression))
|
|
2192
|
+
const listParts = renderedLists.get(node) ?? nestedParts
|
|
2189
2193
|
if (listParts) {
|
|
2190
2194
|
usesBehavior = true
|
|
2191
2195
|
usesList = true
|
|
2192
2196
|
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
|
|
2193
2197
|
listParts.state,
|
|
2194
2198
|
factory.createStringLiteral(listParts.keyField),
|
|
2195
|
-
ts.visitNode(listParts.callback, visitor)
|
|
2199
|
+
ts.visitNode(listParts.callback, visitor),
|
|
2200
|
+
...(nestedParts ? [factory.createStringLiteral(listParts.ownerField)] : [])
|
|
2196
2201
|
]))
|
|
2197
2202
|
}
|
|
2198
2203
|
const conditional = conditionalParts(node.expression)
|
|
@@ -2395,6 +2400,25 @@ function keyedListParts(expression, setters) {
|
|
|
2395
2400
|
return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
|
|
2396
2401
|
}
|
|
2397
2402
|
|
|
2403
|
+
function nestedKeyedListParts(expression, parentItem) {
|
|
2404
|
+
const value = unwrapExpression(expression)
|
|
2405
|
+
if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
|
|
2406
|
+
const collection = value.expression.expression
|
|
2407
|
+
if (!ts.isPropertyAccessExpression(collection) || !ts.isIdentifier(collection.expression) || collection.expression.text !== parentItem) return undefined
|
|
2408
|
+
const callback = value.arguments[0]
|
|
2409
|
+
if (!ts.isArrowFunction(callback) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name)) {
|
|
2410
|
+
throw new Error("Nested keyed list map callback must be an arrow function with one identifier parameter")
|
|
2411
|
+
}
|
|
2412
|
+
const root = unwrapExpression(callback.body)
|
|
2413
|
+
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) throw new Error("Nested keyed list map callback must return one JSX element")
|
|
2414
|
+
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
2415
|
+
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
|
|
2416
|
+
const item = callback.parameters[0].name.text
|
|
2417
|
+
const keyField = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression && directProperty(key.initializer.expression, item)
|
|
2418
|
+
if (!keyField) throw new Error(`Nested keyed list root must have key={${item}.<field>}`)
|
|
2419
|
+
return { callback, root, item, keyField, ownerField: collection.name.text }
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2398
2422
|
function isStateBackedListComponentCall(call, component, setters) {
|
|
2399
2423
|
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
|
|
2400
2424
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
@@ -2479,7 +2503,7 @@ function insideJsxEventHandler(node, root) {
|
|
|
2479
2503
|
return false
|
|
2480
2504
|
}
|
|
2481
2505
|
|
|
2482
|
-
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState) {
|
|
2506
|
+
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists) {
|
|
2483
2507
|
const fail = (node, message) => {
|
|
2484
2508
|
throw sourceNodeError(node, sourceFile, message)
|
|
2485
2509
|
}
|
|
@@ -2490,10 +2514,11 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2490
2514
|
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
|
|
2491
2515
|
}
|
|
2492
2516
|
let conditionDepth = 0
|
|
2517
|
+
let nestedList
|
|
2493
2518
|
const visit = node => {
|
|
2494
2519
|
if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
|
|
2495
2520
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
|
|
2496
|
-
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed
|
|
2521
|
+
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, parts.nested ? "Keyed lists support at most one nested level" : "Nested keyed list collections must be a direct property of the parent item")
|
|
2497
2522
|
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
|
|
2498
2523
|
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
|
|
2499
2524
|
listEventItems.set(node, item)
|
|
@@ -2501,8 +2526,21 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2501
2526
|
}
|
|
2502
2527
|
if (ts.isJsxExpression(node) && node.expression) {
|
|
2503
2528
|
const expression = unwrapExpression(node.expression)
|
|
2529
|
+
if (containsJsx(expression) && ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
|
|
2530
|
+
const nested = nestedKeyedListParts(expression, item)
|
|
2531
|
+
if (!nested) fail(expression, parts.nested ? "Keyed lists support at most one nested level" : "Nested keyed list collections must be a direct property of the parent item")
|
|
2532
|
+
if (parts.nested) fail(expression, "Keyed lists support at most one nested level")
|
|
2533
|
+
if (nestedList) fail(expression, "Keyed list rows support one nested keyed list")
|
|
2534
|
+
if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
|
|
2535
|
+
nestedList = nested
|
|
2536
|
+
const nestedParts = { ...nested, state: parts.state, nested: true }
|
|
2537
|
+
nestedLists.set(expression, nestedParts)
|
|
2538
|
+
validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists)
|
|
2539
|
+
return
|
|
2540
|
+
}
|
|
2504
2541
|
const condition = conditionalParts(expression)
|
|
2505
2542
|
if (condition && containsJsx(expression)) {
|
|
2543
|
+
if (parts.nested) fail(node, "Nested keyed list item conditions are not supported")
|
|
2506
2544
|
if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
|
|
2507
2545
|
if (rowState && referencedStateNames(condition.condition, setters).has(rowState.state)) {
|
|
2508
2546
|
conditionDepth++
|
package/framework/core.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function nativeBehavior(module: string, handler: string, states: Array<[s
|
|
|
27
27
|
export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
28
28
|
export function bindingValue(value: unknown): unknown
|
|
29
29
|
export function conditional(kind: "and" | "ternary", value: unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
30
|
-
export function list(items: unknown, keyField: string, render: (item: unknown) => unknown): unknown
|
|
30
|
+
export function list(items: unknown, keyField: string, render: (item: unknown) => unknown, ownerField?: string): unknown
|
|
31
31
|
export function listField(read: () => unknown, field: string): unknown
|
|
32
32
|
export function listExpression(read: () => unknown, module: string, handler: string): unknown
|
|
33
33
|
export function listItem(): unknown
|
package/framework/core.mjs
CHANGED
|
@@ -214,10 +214,18 @@ export function stateConditional(kind, state, truthy, falsy) {
|
|
|
214
214
|
return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
export function list(items, keyField, render) {
|
|
217
|
+
export function list(items, keyField, render, ownerField) {
|
|
218
218
|
if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state")
|
|
219
|
+
let values = items.value
|
|
220
|
+
if (ownerField) {
|
|
221
|
+
const owner = renderContext?.listRoot ?? renderContext?.listRowRoot
|
|
222
|
+
if (!owner) throw new Error("A nested keyed list must be rendered inside a keyed row")
|
|
223
|
+
renderContext.listFields?.add(ownerField)
|
|
224
|
+
values = renderContext.listTemplate ? [] : owner.item?.[ownerField]
|
|
225
|
+
if (!Array.isArray(values)) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
|
|
226
|
+
}
|
|
219
227
|
const keys = new Set()
|
|
220
|
-
for (const item of
|
|
228
|
+
for (const item of values) {
|
|
221
229
|
const key = item?.[keyField]
|
|
222
230
|
if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
|
|
223
231
|
assertListItem(item)
|
|
@@ -226,7 +234,7 @@ export function list(items, keyField, render) {
|
|
|
226
234
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
227
235
|
keys.add(token)
|
|
228
236
|
}
|
|
229
|
-
return { [listMarker]: true, items, keyField, render }
|
|
237
|
+
return { [listMarker]: true, items, values, keyField, render, ownerField }
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
export function listField(read, field) {
|
|
@@ -347,7 +355,7 @@ function serializeCapture(name, value, seen) {
|
|
|
347
355
|
}
|
|
348
356
|
|
|
349
357
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
350
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowConditions: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
358
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
351
359
|
|
|
352
360
|
try {
|
|
353
361
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -735,23 +743,33 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
735
743
|
|
|
736
744
|
async function renderList(node, namespace, selectValue) {
|
|
737
745
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
738
|
-
const
|
|
739
|
-
const
|
|
746
|
+
const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
|
|
747
|
+
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
748
|
+
const id = node.ownerField ? nextRowListId() : nextRenderId("l")
|
|
749
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map(item => item[node.keyField]), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
750
|
+
if (ownerTemplate) Object.assign(ownerRoot.descriptor, { child: { field: node.ownerField, key: node.keyField }, mount: true, nested: true })
|
|
740
751
|
renderContext.listDepth++
|
|
752
|
+
const previousListRoot = renderContext.listRoot
|
|
753
|
+
const previousListRowRoot = renderContext.listRowRoot
|
|
754
|
+
const previousListTemplate = renderContext.listTemplate
|
|
755
|
+
const previousListInitialMarkers = renderContext.listInitialMarkers
|
|
741
756
|
const previousListFields = renderContext.listFields
|
|
742
757
|
const previousListEffectOwners = renderContext.listEffectOwners
|
|
743
758
|
const previousListRowStates = renderContext.listRowStates
|
|
744
759
|
const previousListRowConditions = renderContext.listRowConditions
|
|
760
|
+
const previousListRowLists = renderContext.listRowLists
|
|
745
761
|
try {
|
|
746
762
|
renderContext.listTemplate = true
|
|
747
763
|
renderContext.listEffectOwners = []
|
|
748
764
|
renderContext.listRowStates = []
|
|
749
765
|
renderContext.listRowConditions = []
|
|
766
|
+
renderContext.listRowLists = []
|
|
750
767
|
renderContext.listFields = new Set([node.keyField])
|
|
751
|
-
renderContext.listRoot = { id, state: node.items.id, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0 } }
|
|
768
|
+
renderContext.listRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
|
|
752
769
|
renderContext.listRowRoot = renderContext.listRoot
|
|
753
770
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
754
|
-
if (template.includes("data-k-native-") || template.includes("data-k-effects=")) descriptor.mount = true
|
|
771
|
+
if (template.includes("data-k-native-") || template.includes("data-k-effects=") || template.includes("data-k-list=")) descriptor.mount = true
|
|
772
|
+
if (template.includes("data-k-list=")) descriptor.nested = true
|
|
755
773
|
if (template.includes("data-k-effects=")) descriptor.effects = true
|
|
756
774
|
if (template.includes("data-k-list-condition")) descriptor.conditions = true
|
|
757
775
|
if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
|
|
@@ -764,33 +782,47 @@ async function renderList(node, namespace, selectValue) {
|
|
|
764
782
|
if (renderContext.listRowConditions.length) descriptor.rowConditions = renderContext.listRowConditions.map(({ id }) => id)
|
|
765
783
|
descriptor.mount = true
|
|
766
784
|
}
|
|
767
|
-
const seed = listSeed(node.
|
|
785
|
+
const seed = node.ownerField ? undefined : listSeed(node.values, renderContext.listFields)
|
|
768
786
|
if (seed) descriptor.seed = seed
|
|
769
787
|
let current = ""
|
|
770
788
|
renderContext.listTemplate = false
|
|
771
789
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
772
|
-
for (const item of node.
|
|
773
|
-
renderContext.listRoot = { id, state: node.items.id, key: item[node.keyField], template: false, effects: [], item, rowIndexes: { s: 0, c: 0 } }
|
|
790
|
+
for (const item of node.values) {
|
|
791
|
+
renderContext.listRoot = { id, state: node.items.id, descriptor, key: item[node.keyField], template: false, effects: [], item, rowIndexes: { s: 0, c: 0, l: 0 } }
|
|
774
792
|
renderContext.listRowRoot = renderContext.listRoot
|
|
775
793
|
current += await renderNode(node.render(item), namespace, selectValue)
|
|
776
794
|
}
|
|
777
|
-
renderContext.lists.push(descriptor)
|
|
795
|
+
if (!node.ownerField || ownerTemplate) renderContext.lists.push(descriptor)
|
|
778
796
|
renderContext.hasBehaviors = true
|
|
779
797
|
renderContext.hasLists = true
|
|
780
798
|
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${template}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
781
799
|
} finally {
|
|
782
|
-
renderContext.listRoot =
|
|
783
|
-
renderContext.listRowRoot =
|
|
784
|
-
renderContext.listTemplate =
|
|
785
|
-
renderContext.listInitialMarkers =
|
|
800
|
+
renderContext.listRoot = previousListRoot
|
|
801
|
+
renderContext.listRowRoot = previousListRowRoot
|
|
802
|
+
renderContext.listTemplate = previousListTemplate
|
|
803
|
+
renderContext.listInitialMarkers = previousListInitialMarkers
|
|
786
804
|
renderContext.listFields = previousListFields
|
|
787
805
|
renderContext.listEffectOwners = previousListEffectOwners
|
|
788
806
|
renderContext.listRowStates = previousListRowStates
|
|
789
807
|
renderContext.listRowConditions = previousListRowConditions
|
|
808
|
+
renderContext.listRowLists = previousListRowLists
|
|
790
809
|
renderContext.listDepth--
|
|
791
810
|
}
|
|
792
811
|
}
|
|
793
812
|
|
|
813
|
+
function nextRowListId() {
|
|
814
|
+
const root = renderContext.listRoot ?? renderContext.listRowRoot
|
|
815
|
+
const index = root.rowIndexes.l++
|
|
816
|
+
if (renderContext.listTemplate) {
|
|
817
|
+
const id = nextRenderId("l")
|
|
818
|
+
renderContext.listRowLists[index] = { id }
|
|
819
|
+
return id
|
|
820
|
+
}
|
|
821
|
+
const entry = renderContext.listRowLists[index]
|
|
822
|
+
if (!entry) throw new Error("Nested keyed lists must have the same order for every parent item")
|
|
823
|
+
return entry.id
|
|
824
|
+
}
|
|
825
|
+
|
|
794
826
|
function nextRenderId(kind) {
|
|
795
827
|
if (renderContext.scoped) return `${renderContext.renderScope === "layout" ? "l" : "r"}${kind}${renderContext.counters[renderContext.renderScope][kind]++}`
|
|
796
828
|
const counters = { s: "nextState", r: "nextRef", c: "nextCondition", l: "nextList", e: "nextEffect", p: "nextParam" }
|
|
@@ -7,6 +7,7 @@ const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
|
7
7
|
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
8
8
|
const itemParts = new WeakMap()
|
|
9
9
|
const listItems = new WeakMap()
|
|
10
|
+
const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
|
|
10
11
|
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
11
12
|
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
|
|
12
13
|
|
|
@@ -37,8 +38,8 @@ function mountLists(root) {
|
|
|
37
38
|
const roots = listRoots(start, end)
|
|
38
39
|
if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
|
|
39
40
|
const templateRoot = start.content.firstElementChild
|
|
40
|
-
const parts = listItemPartPlan(templateRoot)
|
|
41
|
-
for (const root of roots) __KUDZU_LIST_CONDITIONS__ && descriptor.conditions ? listItemParts(root) : mapListItemParts(parts, root)
|
|
41
|
+
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
42
|
+
for (const root of roots) __KUDZU_LIST_CONDITIONS__ && descriptor.conditions ? listItemParts(root, descriptor.nested) : mapListItemParts(parts, root, descriptor.nested)
|
|
42
43
|
if (__KUDZU_LIST_SEEDS__ && descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
43
44
|
const items = browserState.get(descriptor.state)
|
|
44
45
|
const list = {
|
|
@@ -50,10 +51,18 @@ function mountLists(root) {
|
|
|
50
51
|
values: new Map(),
|
|
51
52
|
items: undefined,
|
|
52
53
|
container: roots[0]?.parentNode,
|
|
53
|
-
boundary: end
|
|
54
|
+
boundary: end,
|
|
55
|
+
...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner: listOwner(start) } : {})
|
|
56
|
+
}
|
|
57
|
+
if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) {
|
|
58
|
+
if (!list.owner) throw new Error("Nested keyed list has no parent row")
|
|
59
|
+
if (ownedLists.has(list.owner)) throw new Error("Keyed list rows support one nested keyed list")
|
|
60
|
+
ownedLists.set(list.owner, list)
|
|
61
|
+
listRegistrations.set(start, { list, owner: list.owner })
|
|
62
|
+
} else {
|
|
63
|
+
register(listTargets, descriptor.state, list)
|
|
64
|
+
listRegistrations.set(start, { state: descriptor.state, list })
|
|
54
65
|
}
|
|
55
|
-
register(listTargets, descriptor.state, list)
|
|
56
|
-
listRegistrations.set(start, { state: descriptor.state, list })
|
|
57
66
|
updateList(list)
|
|
58
67
|
}
|
|
59
68
|
}
|
|
@@ -65,9 +74,13 @@ function unmountLists(root) {
|
|
|
65
74
|
function unregisterList(start) {
|
|
66
75
|
const registration = listRegistrations.get(start)
|
|
67
76
|
if (registration) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
if (__KUDZU_NESTED_LISTS__ && registration.owner) {
|
|
78
|
+
if (ownedLists.get(registration.owner) === registration.list) ownedLists.delete(registration.owner)
|
|
79
|
+
} else {
|
|
80
|
+
const lists = listTargets.get(registration.state)
|
|
81
|
+
lists?.delete(registration.list)
|
|
82
|
+
if (!lists?.size) listTargets.delete(registration.state)
|
|
83
|
+
}
|
|
71
84
|
if (__KUDZU_LIST_ROW_STATES__ && registration.list.descriptor.rowStates) for (const token of registration.list.roots.keys()) deleteRowStates(registration.list.descriptor, token)
|
|
72
85
|
}
|
|
73
86
|
listRegistrations.delete(start)
|
|
@@ -75,8 +88,12 @@ function unregisterList(start) {
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
function updateList(list) {
|
|
78
|
-
const items =
|
|
79
|
-
|
|
91
|
+
const items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
|
|
92
|
+
? listItems.get(list.owner)?.[list.descriptor.ownerField]
|
|
93
|
+
: browserState.get(list.descriptor.state)
|
|
94
|
+
if (!Array.isArray(items)) throw new Error(list.descriptor.ownerField ? `Nested keyed list property "${list.descriptor.ownerField}" must remain an array` : "Keyed list state must remain an array")
|
|
95
|
+
if (__KUDZU_NESTED_LISTS__ && (list.descriptor.child || list.descriptor.ownerField) && list.items && updateNestedList(list, items)) return
|
|
96
|
+
if (__KUDZU_NESTED_LISTS__ && list.descriptor.child) validateChildLists(items, list.descriptor.child)
|
|
80
97
|
if (list.descriptor.reducer && list.items && updateReducerList(list, items)) return
|
|
81
98
|
const entries = []
|
|
82
99
|
const keys = new Set()
|
|
@@ -132,15 +149,14 @@ function updateList(list) {
|
|
|
132
149
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
133
150
|
node.removeAttribute("data-k-list-root")
|
|
134
151
|
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
135
|
-
mapListItemParts(list.parts, node)
|
|
136
|
-
fillListItem(node, item)
|
|
152
|
+
mapListItemParts(list.parts, node, list.descriptor.nested)
|
|
153
|
+
fillListItem(node, item, list.descriptor.nested)
|
|
137
154
|
additions.append(node)
|
|
138
155
|
added = true
|
|
139
156
|
} else if (list.values.get(token) !== value) {
|
|
140
|
-
fillListItem(node, item)
|
|
157
|
+
fillListItem(node, item, list.descriptor.nested)
|
|
141
158
|
if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
|
|
142
159
|
}
|
|
143
|
-
listItems.set(node, item)
|
|
144
160
|
next.push([token, node])
|
|
145
161
|
values.set(token, value)
|
|
146
162
|
}
|
|
@@ -184,6 +200,75 @@ function updateList(list) {
|
|
|
184
200
|
list.items = items
|
|
185
201
|
}
|
|
186
202
|
|
|
203
|
+
function updateNestedList(list, items) {
|
|
204
|
+
const previous = list.items
|
|
205
|
+
if (items === previous) return false
|
|
206
|
+
if (items.length === previous.length && items.every((item, index) => item === previous[index])) {
|
|
207
|
+
list.items = items
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
if (items.length === previous.length && items.every((item, index) => item === previous[previous.length - index - 1])) {
|
|
211
|
+
const tokens = [...list.roots.keys()].reverse()
|
|
212
|
+
const parent = list.container ?? list.start.parentNode
|
|
213
|
+
const reordered = parent.ownerDocument.createDocumentFragment()
|
|
214
|
+
reordered.append(...tokens.map(token => list.roots.get(token)))
|
|
215
|
+
parent.insertBefore(reordered, list.boundary)
|
|
216
|
+
list.roots = new Map(tokens.map(token => [token, list.roots.get(token)]))
|
|
217
|
+
list.items = items
|
|
218
|
+
list.container ??= parent
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
if (items.length === previous.length - 1) {
|
|
222
|
+
let removed = 0
|
|
223
|
+
while (removed < items.length && items[removed] === previous[removed]) removed++
|
|
224
|
+
if (items.every((item, index) => item === previous[index >= removed ? index + 1 : index])) {
|
|
225
|
+
removeListRoot(list, keyToken(previous[removed]?.[list.descriptor.key]))
|
|
226
|
+
list.roots = new Map(items.map(item => {
|
|
227
|
+
const token = keyToken(item[list.descriptor.key])
|
|
228
|
+
return [token, list.roots.get(token)]
|
|
229
|
+
}))
|
|
230
|
+
list.items = items
|
|
231
|
+
return true
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (items.length === previous.length + 1 && previous.every((item, index) => item === items[index])) {
|
|
235
|
+
const entry = nestedListEntry(list, items.at(-1))
|
|
236
|
+
if (list.roots.has(entry.token)) throw new Error(`Duplicate keyed list key: ${String(entry.key)}`)
|
|
237
|
+
addListRoot(list, entry)
|
|
238
|
+
list.items = items
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
if (items.length !== previous.length) return false
|
|
242
|
+
let changed = -1
|
|
243
|
+
for (let index = 0; index < items.length; index++) {
|
|
244
|
+
if (items[index] === previous[index]) continue
|
|
245
|
+
if (changed !== -1) return false
|
|
246
|
+
changed = index
|
|
247
|
+
}
|
|
248
|
+
if (changed === -1) return false
|
|
249
|
+
const item = items[changed]
|
|
250
|
+
if (item?.[list.descriptor.key] !== previous[changed]?.[list.descriptor.key]) return false
|
|
251
|
+
const entry = nestedListEntry(list, item)
|
|
252
|
+
const node = list.roots.get(entry.token)
|
|
253
|
+
if (!node) return false
|
|
254
|
+
if (list.values.get(entry.token) !== entry.value) {
|
|
255
|
+
fillListItem(node, item, list.descriptor.nested)
|
|
256
|
+
if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
|
|
257
|
+
list.values.set(entry.token, entry.value)
|
|
258
|
+
}
|
|
259
|
+
list.items = items
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function nestedListEntry(list, item) {
|
|
264
|
+
const key = item?.[list.descriptor.key]
|
|
265
|
+
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
266
|
+
assertListItem(item)
|
|
267
|
+
if (list.descriptor.child) validateChildLists([item], list.descriptor.child)
|
|
268
|
+
assertListValue(item, new Set(), true)
|
|
269
|
+
return { item, key, token: keyToken(key), value: JSON.stringify(item) }
|
|
270
|
+
}
|
|
271
|
+
|
|
187
272
|
function updateReducerList(list, items) {
|
|
188
273
|
const previous = list.items
|
|
189
274
|
if (items.length === previous.length && items.every((item, index) => item === previous[index])) {
|
|
@@ -237,9 +322,8 @@ function addListRoot(list, { item, key, token, value }) {
|
|
|
237
322
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
238
323
|
node.removeAttribute("data-k-list-root")
|
|
239
324
|
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
240
|
-
mapListItemParts(list.parts, node)
|
|
241
|
-
fillListItem(node, item)
|
|
242
|
-
listItems.set(node, item)
|
|
325
|
+
mapListItemParts(list.parts, node, list.descriptor.nested)
|
|
326
|
+
fillListItem(node, item, list.descriptor.nested)
|
|
243
327
|
const parent = list.container ?? list.start.parentNode
|
|
244
328
|
parent.insertBefore(node, list.boundary)
|
|
245
329
|
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
|
|
@@ -257,11 +341,16 @@ function removeListRoot(list, token) {
|
|
|
257
341
|
list.values.delete(token)
|
|
258
342
|
}
|
|
259
343
|
|
|
260
|
-
function fillListItem(root, item) {
|
|
344
|
+
function fillListItem(root, item, nested = false) {
|
|
345
|
+
listItems.set(root, item)
|
|
261
346
|
const revision = __KUDZU_LIST_ASYNC_PARTS__ ? (revisions.get(root) ?? 0) + 1 : 0
|
|
262
347
|
if (__KUDZU_LIST_ASYNC_PARTS__) revisions.set(root, revision)
|
|
263
|
-
const parts = listItemParts(root)
|
|
348
|
+
const parts = listItemParts(root, nested)
|
|
264
349
|
fillListParts(root, parts, item, revision)
|
|
350
|
+
if (__KUDZU_NESTED_LISTS__) {
|
|
351
|
+
const child = ownedLists.get(root)
|
|
352
|
+
if (child) updateList(child)
|
|
353
|
+
}
|
|
265
354
|
}
|
|
266
355
|
|
|
267
356
|
function fillListParts(root, parts, item, revision) {
|
|
@@ -317,11 +406,11 @@ function fillListParts(root, parts, item, revision) {
|
|
|
317
406
|
}
|
|
318
407
|
}
|
|
319
408
|
|
|
320
|
-
function listItemParts(root) {
|
|
409
|
+
function listItemParts(root, nested = false) {
|
|
321
410
|
let parts = itemParts.get(root)
|
|
322
411
|
if (parts) return parts
|
|
323
412
|
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [], effects: [] }
|
|
324
|
-
for (const node of matching(root, itemPartsSelector)) {
|
|
413
|
+
for (const node of nested ? ownedElements(root).filter(node => node.matches(itemPartsSelector)) : matching(root, itemPartsSelector)) {
|
|
325
414
|
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
326
415
|
if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
327
416
|
if (__KUDZU_LIST_EVENTS__ && node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
|
|
@@ -337,8 +426,8 @@ function listItemParts(root) {
|
|
|
337
426
|
return parts
|
|
338
427
|
}
|
|
339
428
|
|
|
340
|
-
function listItemPartPlan(template) {
|
|
341
|
-
const source = [template, ...template.querySelectorAll("*")]
|
|
429
|
+
function listItemPartPlan(template, nested = false) {
|
|
430
|
+
const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
|
|
342
431
|
const indexes = new Map(source.map((node, index) => [node, index]))
|
|
343
432
|
const parts = listItemParts(template)
|
|
344
433
|
return {
|
|
@@ -353,8 +442,8 @@ function listItemPartPlan(template) {
|
|
|
353
442
|
}
|
|
354
443
|
}
|
|
355
444
|
|
|
356
|
-
function mapListItemParts(parts, root) {
|
|
357
|
-
const target = [root, ...root.querySelectorAll("*")]
|
|
445
|
+
function mapListItemParts(parts, root, nested = false) {
|
|
446
|
+
const target = nested ? ownedElements(root) : [root, ...root.querySelectorAll("*")]
|
|
358
447
|
itemParts.set(root, {
|
|
359
448
|
directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
|
|
360
449
|
texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([index, field]) => [target[index], field]) : [],
|
|
@@ -406,6 +495,49 @@ function renderFalsy(value) {
|
|
|
406
495
|
return value === false || value == null || value === true ? "" : String(value)
|
|
407
496
|
}
|
|
408
497
|
|
|
498
|
+
function validateChildLists(items, child) {
|
|
499
|
+
for (const item of items) {
|
|
500
|
+
const children = item?.[child.field]
|
|
501
|
+
if (!Array.isArray(children)) throw new Error(`Nested keyed list property "${child.field}" must remain an array`)
|
|
502
|
+
const keys = new Set()
|
|
503
|
+
for (const entry of children) {
|
|
504
|
+
const key = entry?.[child.key]
|
|
505
|
+
if (!validListKey(key)) throw new Error(`Keyed list key "${child.key}" must be a string or finite number`)
|
|
506
|
+
assertListItem(entry)
|
|
507
|
+
assertListValue(entry, new Set(), true)
|
|
508
|
+
const token = keyToken(key)
|
|
509
|
+
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
510
|
+
keys.add(token)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function listOwner(start) {
|
|
516
|
+
let owner = start.parentElement
|
|
517
|
+
while (owner && !listItems.has(owner)) owner = owner.parentElement
|
|
518
|
+
return owner
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function ownedElements(root) {
|
|
522
|
+
const elements = []
|
|
523
|
+
const visit = node => {
|
|
524
|
+
elements.push(node)
|
|
525
|
+
for (let child = node.firstElementChild; child;) {
|
|
526
|
+
if (child.matches("template[data-k-list]")) {
|
|
527
|
+
const end = findEnd(child, JSON.parse(child.dataset.kList).id)
|
|
528
|
+
elements.push(child, end)
|
|
529
|
+
child = end.nextElementSibling
|
|
530
|
+
} else {
|
|
531
|
+
const next = child.nextElementSibling
|
|
532
|
+
visit(child)
|
|
533
|
+
child = next
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
visit(root)
|
|
538
|
+
return elements
|
|
539
|
+
}
|
|
540
|
+
|
|
409
541
|
function listRoots(start, end) {
|
|
410
542
|
const roots = []
|
|
411
543
|
for (let node = start.nextSibling; node && node !== end; node = node.nextSibling) {
|
|
@@ -585,8 +717,10 @@ function assertListValue(value, seen, root = false) {
|
|
|
585
717
|
}
|
|
586
718
|
|
|
587
719
|
function findEnd(start, id) {
|
|
588
|
-
|
|
589
|
-
.
|
|
720
|
+
for (let node = start.nextElementSibling; node; node = node.nextElementSibling) {
|
|
721
|
+
if (node.matches("template[data-k-list-end]") && node.dataset.kListEnd === id) return node
|
|
722
|
+
}
|
|
723
|
+
throw new Error("Keyed list marker has no end")
|
|
590
724
|
}
|
|
591
725
|
|
|
592
726
|
function register(targets, id, entry) {
|