@kudzujs/core 0.6.27 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
2
+ import { selectCollection } from "./collection-selector.js"
2
3
 
3
4
  const listTargets = new Map()
4
5
  const listRegistrations = new WeakMap()
@@ -7,6 +8,9 @@ const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
7
8
  const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
8
9
  const itemParts = new WeakMap()
9
10
  const listItems = new WeakMap()
11
+ const listIndexes = __KUDZU_LIST_INDEXES__ ? new WeakMap() : undefined
12
+ const ownershipPaths = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
13
+ const rowReplacements = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
10
14
  const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
11
15
  const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
12
16
  const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
@@ -32,7 +36,7 @@ if (typeof document !== "undefined") mountDom(document)
32
36
  function mountLists(root) {
33
37
  let owner = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement
34
38
  while (owner && !listItems.has(owner)) owner = owner.parentElement
35
- if (owner) fillListParts(root, listItemParts(root), listItems.get(owner), 0)
39
+ if (owner) fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)
36
40
  for (const start of matching(root, "template[data-k-list]")) {
37
41
  if (mountedLists.has(start)) continue
38
42
  mountedLists.add(start)
@@ -41,7 +45,7 @@ function mountLists(root) {
41
45
  const roots = listRoots(start, end)
42
46
  const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
43
47
  const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start.content.firstElementChild
44
- if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
48
+ if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
45
49
  const parts = listItemPartPlan(templateRoot, descriptor.nested)
46
50
  for (const root of roots) {
47
51
  if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
@@ -55,10 +59,11 @@ function mountLists(root) {
55
59
  const list = {
56
60
  start,
57
61
  descriptor,
58
- ...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototype ? { childPrototype: nested.childPrototype } : {}) } : {}),
62
+ ...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}) } : {}),
59
63
  parts,
60
64
  seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
61
65
  roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
66
+ ...(__KUDZU_LIST_STABLE_FAST_PATHS__ ? { orderedRoots: roots } : {}),
62
67
  values: new Map(),
63
68
  items: undefined,
64
69
  container: roots[0]?.parentNode,
@@ -67,8 +72,10 @@ function mountLists(root) {
67
72
  }
68
73
  if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) {
69
74
  if (!list.owner) throw new Error("Nested keyed list has no parent row")
70
- if (ownedLists.has(list.owner)) throw new Error("Keyed list rows support one nested keyed list")
71
- ownedLists.set(list.owner, list)
75
+ const lists = ownedLists.get(list.owner) ?? new Map()
76
+ if (lists.has(descriptor.id)) throw new Error(`Duplicate nested keyed list ID: ${descriptor.id}`)
77
+ lists.set(descriptor.id, list)
78
+ ownedLists.set(list.owner, lists)
72
79
  listRegistrations.set(start, { list, owner: list.owner })
73
80
  } else {
74
81
  register(listTargets, descriptor.state, list)
@@ -86,41 +93,50 @@ function unregisterList(start) {
86
93
  const registration = listRegistrations.get(start)
87
94
  if (registration) {
88
95
  if (__KUDZU_NESTED_LISTS__ && registration.owner) {
89
- if (ownedLists.get(registration.owner) === registration.list) ownedLists.delete(registration.owner)
96
+ const lists = ownedLists.get(registration.owner)
97
+ if (lists?.get(registration.list.descriptor.id) === registration.list) lists.delete(registration.list.descriptor.id)
98
+ if (!lists?.size) ownedLists.delete(registration.owner)
90
99
  } else {
91
100
  const lists = listTargets.get(registration.state)
92
101
  lists?.delete(registration.list)
93
102
  if (!lists?.size) listTargets.delete(registration.state)
94
103
  }
95
- if (__KUDZU_LIST_ROW_STATES__ && registration.list.descriptor.rowStates) for (const token of registration.list.roots.keys()) deleteRowStates(registration.list.descriptor, token)
104
+ if (__KUDZU_LIST_ROW_HOOKS__ && registration.list.descriptor.rowStates) for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))
96
105
  }
97
106
  listRegistrations.delete(start)
98
107
  mountedLists.delete(start)
99
108
  }
100
109
 
101
110
  function updateList(list) {
102
- const items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
111
+ let items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
103
112
  ? listItems.get(list.owner)?.[list.descriptor.ownerField]
104
113
  : browserState.get(list.descriptor.state)
114
+ if (__KUDZU_NESTED_LISTS__ && list.descriptor.ownerField && items == null) items = []
115
+ if (__KUDZU_COLLECTION_SELECTORS__) items = selectCollection(items, list.descriptor.selector)
105
116
  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")
106
- if (__KUDZU_NESTED_LISTS__ && (list.descriptor.child || list.descriptor.ownerField) && list.items && updateNestedList(list, items)) return
107
- if (__KUDZU_NESTED_LISTS__ && list.descriptor.child) validateChildLists(items, list.descriptor.child)
108
- if (list.descriptor.reducer && list.items && updateReducerList(list, items)) return
117
+ if (__KUDZU_NESTED_LISTS__ && (list.descriptor.children || list.descriptor.ownerField) && !list.descriptor.indexed && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateNestedList(list, items)) return
118
+ if (__KUDZU_NESTED_LISTS__ && list.descriptor.children) validateChildLists(items, list.descriptor.children)
119
+ if (list.descriptor.reducer && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateReducerList(list, items)) return
120
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__ && list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && !list.descriptor.reducer && !list.descriptor.children && !list.descriptor.ownerField && list.items && updateStableList(list, items)) return
109
121
  const entries = []
110
122
  const keys = new Set()
111
123
  const seen = new Set()
112
- for (const item of items) {
113
- const key = item?.[list.descriptor.key]
124
+ const referenceOnly = usesItemReferences(list)
125
+ for (const [index, item] of items.entries()) {
126
+ const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]
114
127
  if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
115
128
  assertListItem(item)
116
- const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
129
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
117
130
  if (seededValue === undefined) assertListValue(item, seen, true)
118
131
  const token = keyToken(key)
119
132
  if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
120
133
  keys.add(token)
121
- entries.push({ item, key, token, value: seededValue === undefined ? JSON.stringify(item) : seededValue })
134
+ entries.push({ item, index, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
122
135
  }
123
- if (list.values.size && entries.every(entry => !list.values.has(entry.token) || list.values.get(entry.token) === entry.value)) {
136
+ if (list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && (referenceOnly || list.values.size) && entries.every(entry => {
137
+ const root = list.roots.get(entry.token)
138
+ return !root || (referenceOnly ? listItems.get(root) === entry.item : list.values.get(entry.token) === entry.value)
139
+ })) {
124
140
  const currentTokens = [...list.roots.keys()]
125
141
  const nextTokens = entries.map(entry => entry.token)
126
142
  if (nextTokens.length === currentTokens.length && nextTokens.every((token, index) => token === currentTokens[currentTokens.length - index - 1])) {
@@ -129,6 +145,7 @@ function updateList(list) {
129
145
  reordered.append(...nextTokens.map(token => list.roots.get(token)))
130
146
  parent.insertBefore(reordered, list.boundary)
131
147
  list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
148
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = nextTokens.map(token => list.roots.get(token))
132
149
  list.container ??= parent
133
150
  return
134
151
  }
@@ -138,12 +155,14 @@ function updateList(list) {
138
155
  if (removed && nextTokens.every((token, index) => token === currentTokens[index >= removedIndex ? index + 1 : index])) {
139
156
  removeListRoot(list, removed)
140
157
  list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
158
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = nextTokens.map(token => list.roots.get(token))
141
159
  return
142
160
  }
143
161
  }
144
162
  if (nextTokens.length === currentTokens.length + 1 && currentTokens.every((token, index) => token === nextTokens[index])) {
145
163
  const entry = entries.at(-1)
146
164
  addListRoot(list, entry)
165
+ list.items = items
147
166
  return
148
167
  }
149
168
  }
@@ -152,25 +171,25 @@ function updateList(list) {
152
171
  const parent = list.container ?? list.start.parentNode
153
172
  const additions = parent.ownerDocument.createDocumentFragment()
154
173
  let added = false
155
- for (const { item, key, token, value } of entries) {
174
+ for (const { item, index, key, token, value } of entries) {
156
175
  let node = list.roots.get(token)
157
176
  if (!node) {
158
177
  node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
159
178
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
160
179
  if (!node) throw new Error("Keyed list template has no root element")
161
180
  node.removeAttribute("data-k-list-root")
162
- if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
163
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
181
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
182
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
164
183
  mapListItemParts(list.parts, node, list.descriptor.nested)
165
- fillListItem(node, item, list.descriptor.nested)
184
+ fillListItem(node, item, list.descriptor.nested, index)
166
185
  additions.append(node)
167
186
  added = true
168
- } else if (list.values.get(token) !== value) {
169
- fillListItem(node, item, list.descriptor.nested)
187
+ } else if ((referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null) {
188
+ fillListItem(node, item, list.descriptor.nested, index)
170
189
  if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
171
190
  }
172
191
  next.push([token, node])
173
- values.set(token, value)
192
+ if (!referenceOnly) values.set(token, value)
174
193
  }
175
194
  for (const [token, node] of list.roots) {
176
195
  if (keys.has(token)) continue
@@ -178,7 +197,7 @@ function updateList(list) {
178
197
  unmountDom(node)
179
198
  node.remove()
180
199
  } else node.remove()
181
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
200
+ if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
182
201
  }
183
202
  if (added) {
184
203
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
@@ -208,10 +227,102 @@ function updateList(list) {
208
227
  }
209
228
  }
210
229
  list.roots = new Map(next)
230
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = next.map(([, node]) => node)
211
231
  list.values = values
212
232
  list.items = items
213
233
  }
214
234
 
235
+ /* stable-list-fast-path */
236
+ function updateStableList(list, items) {
237
+ const previous = list.items
238
+ const referenceOnly = usesItemReferences(list)
239
+ if (items.length < previous.length) return false
240
+ const appending = items.length > previous.length
241
+ const keys = new Set()
242
+ const seen = new Set()
243
+ const keyField = list.descriptor.key
244
+ const fragment = list.start.ownerDocument.createDocumentFragment()
245
+ let stable = true
246
+ for (let index = 0; index < items.length; index++) {
247
+ const item = items[index]
248
+ assertListItem(item)
249
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
250
+ if (seededValue === undefined) assertListValue(item, seen, true)
251
+ const key = item[keyField]
252
+ if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
253
+ const token = keyToken(key)
254
+ if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
255
+ keys.add(token)
256
+ if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index])) stable = false
257
+ if (!stable || items.length === previous.length + 1 || index < previous.length) continue
258
+ let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
259
+ if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
260
+ if (!node) throw new Error("Keyed list template has no root element")
261
+ node.removeAttribute("data-k-list-root")
262
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
263
+ if (list.parts.directFill) fillStructuralListParts(list.parts, node, item)
264
+ else fillListItem(node, item, list.descriptor.nested, index, mapListItemParts(list.parts, node, list.descriptor.nested))
265
+ fragment.append(node)
266
+ }
267
+ if (!stable) return false
268
+ if (appending) {
269
+ if (items.length === previous.length + 1) {
270
+ const item = items.at(-1)
271
+ const key = item[keyField]
272
+ const token = keyToken(key)
273
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed, !referenceOnly) : undefined
274
+ addListRoot(list, { item, index: previous.length, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
275
+ if (referenceOnly) list.values.clear()
276
+ list.items = items
277
+ return true
278
+ }
279
+ const parent = list.container ?? list.start.parentNode
280
+ let node = fragment.firstChild
281
+ for (let index = previous.length; node; index++) {
282
+ const item = items[index]
283
+ const token = keyToken(item[keyField])
284
+ list.roots.set(token, node)
285
+ list.orderedRoots.push(node)
286
+ if (!referenceOnly) {
287
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
288
+ list.values.set(token, seededValue === undefined ? JSON.stringify(item) : seededValue)
289
+ }
290
+ node = node.nextSibling
291
+ }
292
+ const firstAdded = fragment.firstChild
293
+ parent.insertBefore(fragment, list.boundary)
294
+ if (referenceOnly) list.values.clear()
295
+ if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) for (let node = firstAdded; node !== list.boundary; node = node.nextSibling) mountDom(node)
296
+ list.container ??= parent
297
+ list.items = items
298
+ return true
299
+ }
300
+ if (referenceOnly) {
301
+ for (let index = 0; index < items.length; index++) {
302
+ const item = items[index]
303
+ if (item !== previous[index]) fillListItem(list.orderedRoots[index], item, list.descriptor.nested, index)
304
+ }
305
+ } else {
306
+ for (let index = 0; index < items.length; index++) {
307
+ const item = items[index]
308
+ if (item === previous[index]) continue
309
+ const token = keyToken(item[keyField])
310
+ const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
311
+ const value = seededValue === undefined ? JSON.stringify(item) : seededValue
312
+ if (list.values.get(token) !== value) {
313
+ const node = list.orderedRoots[index]
314
+ fillListItem(node, item, list.descriptor.nested, index)
315
+ if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
316
+ list.values.set(token, value)
317
+ }
318
+ }
319
+ }
320
+ if (referenceOnly) list.values.clear()
321
+ list.items = items
322
+ return true
323
+ }
324
+ /* stable-list-fast-path-end */
325
+
215
326
  function updateNestedList(list, items) {
216
327
  const previous = list.items
217
328
  if (items === previous) return false
@@ -276,7 +387,7 @@ function nestedListEntry(list, item) {
276
387
  const key = item?.[list.descriptor.key]
277
388
  if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
278
389
  assertListItem(item)
279
- if (list.descriptor.child) validateChildLists([item], list.descriptor.child)
390
+ if (list.descriptor.children) validateChildLists([item], list.descriptor.children)
280
391
  assertListValue(item, new Set(), true)
281
392
  return { item, key, token: keyToken(key), value: JSON.stringify(item) }
282
393
  }
@@ -328,20 +439,21 @@ function updateReducerList(list, items) {
328
439
  return false
329
440
  }
330
441
 
331
- function addListRoot(list, { item, key, token, value }) {
442
+ function addListRoot(list, { item, index = list.roots.size, key, token, value }) {
332
443
  let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
333
444
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
334
445
  if (!node) throw new Error("Keyed list template has no root element")
335
446
  node.removeAttribute("data-k-list-root")
336
- if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
337
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
447
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
448
+ if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
338
449
  mapListItemParts(list.parts, node, list.descriptor.nested)
339
- fillListItem(node, item, list.descriptor.nested)
450
+ fillListItem(node, item, list.descriptor.nested, index)
340
451
  const parent = list.container ?? list.start.parentNode
341
452
  parent.insertBefore(node, list.boundary)
342
453
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
343
454
  list.roots.set(token, node)
344
- list.values.set(token, value)
455
+ if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots.push(node)
456
+ if (!usesItemReferences(list)) list.values.set(token, value)
345
457
  list.container ??= parent
346
458
  }
347
459
 
@@ -349,24 +461,25 @@ function removeListRoot(list, token) {
349
461
  const node = list.roots.get(token)
350
462
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) unmountDom(node)
351
463
  node.remove()
352
- if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
464
+ if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
353
465
  list.roots.delete(token)
354
466
  list.values.delete(token)
355
467
  }
356
468
 
357
- function fillListItem(root, item, nested = false) {
469
+ function fillListItem(root, item, nested = false, index = 0, parts = listItemParts(root, nested)) {
358
470
  listItems.set(root, item)
471
+ if (__KUDZU_LIST_INDEXES__) listIndexes.set(root, index)
359
472
  const revision = __KUDZU_LIST_ASYNC_PARTS__ ? (revisions.get(root) ?? 0) + 1 : 0
360
473
  if (__KUDZU_LIST_ASYNC_PARTS__) revisions.set(root, revision)
361
- const parts = listItemParts(root, nested)
362
- fillListParts(root, parts, item, revision)
474
+ fillListParts(root, parts, item, revision, index)
363
475
  if (__KUDZU_NESTED_LISTS__) {
364
- const child = ownedLists.get(root)
365
- if (child) updateList(child)
476
+ const children = ownedLists.get(root)
477
+ if (children) for (const child of children.values()) updateList(child)
366
478
  }
479
+ if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))
367
480
  }
368
481
 
369
- function fillListParts(root, parts, item, revision) {
482
+ function fillListParts(root, parts, item, revision, index = 0) {
370
483
  if (__KUDZU_LIST_EFFECTS__) for (const node of parts.effects) node.dataset.kEffectItem = JSON.stringify(item)
371
484
  for (const [node, field] of parts.directTexts) {
372
485
  const text = item?.[field]
@@ -389,31 +502,31 @@ function fillListParts(root, parts, item, revision) {
389
502
  if (__KUDZU_LIST_EVENTS__) {
390
503
  for (const [node, events] of parts.events) {
391
504
  for (const [event, native] of JSON.parse(events)) {
392
- native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
505
+ native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value]))
393
506
  node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
394
507
  }
395
508
  }
396
509
  }
397
510
  if (__KUDZU_LIST_EXPRESSIONS__) {
398
511
  for (const [marker, descriptor] of parts.expressions) {
399
- evaluate(descriptor, item).then(value => {
400
- if (revisions.get(root) === revision && root.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
512
+ evaluate(descriptor, item, index).then(value => {
513
+ if (revisions.get(root) === revision && marker.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
401
514
  }).catch(error => console.error(error))
402
515
  }
403
516
  }
404
517
  if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__) {
405
518
  for (const [node, attributes] of parts.expressionAttributes) {
406
519
  for (const [target, module, handler] of attributes) {
407
- evaluate({ module, handler }, item).then(value => {
408
- if (revisions.get(root) === revision && root.isConnected) patchBinding(node, target, value)
520
+ evaluate({ module, handler }, item, index).then(value => {
521
+ if (revisions.get(root) === revision && node.isConnected) patchBinding(node, target, value)
409
522
  }).catch(error => console.error(error))
410
523
  }
411
524
  }
412
525
  }
413
526
  if (__KUDZU_LIST_CONDITIONS__) {
414
527
  for (const [marker, descriptor] of parts.conditions) {
415
- evaluate(descriptor, item).then(value => {
416
- if (revisions.get(root) === revision && root.isConnected) updateListCondition(marker, descriptor.kind, value, item)
528
+ evaluate(descriptor, item, index).then(value => {
529
+ if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
417
530
  }).catch(error => console.error(error))
418
531
  }
419
532
  }
@@ -462,21 +575,45 @@ function listItemPartPlan(template, nested = false) {
462
575
  const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
463
576
  const indexes = new Map(source.map((node, index) => [node, index]))
464
577
  const parts = listItemParts(template, nested)
578
+ const structural = !nested && !parts.texts.length && !parts.expressions.length && !parts.expressionAttributes.length && !parts.conditions.length && !parts.effects.length
579
+ const location = structural ? node => elementPath(template, node) : node => indexes.get(node)
465
580
  return {
466
- directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
467
- texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [indexes.get(node), field]) : [],
468
- attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
469
- events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
470
- expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
471
- expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
472
- conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor, node]) : [],
473
- effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
581
+ structural,
582
+ directFill: structural && !parts.events.length && !__KUDZU_LIST_ASYNC_PARTS__ && !__KUDZU_NESTED_LISTS__ && !__KUDZU_LIST_INDEXES__ && !__KUDZU_LIST_ROW_HOOKS__,
583
+ directTexts: parts.directTexts.map(([node, field]) => [location(node), field]),
584
+ texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [location(node), field]) : [],
585
+ attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [location(node), attributes]) : [],
586
+ events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [location(node), events]) : [],
587
+ expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [location(node), descriptor]) : [],
588
+ expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [location(node), attributes]) : [],
589
+ conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [location(node), descriptor, node]) : [],
590
+ effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(location) : []
474
591
  }
475
592
  }
476
593
 
477
594
  function mapListItemParts(parts, root, nested = false) {
595
+ if (parts.structural) {
596
+ const target = path => {
597
+ if (!path.length) return root
598
+ let node = root.children[path[0]]
599
+ for (let index = 1; index < path.length; index++) node = node.children[path[index]]
600
+ return node
601
+ }
602
+ const mapped = {
603
+ directTexts: parts.directTexts.map(([path, field]) => [target(path), field]),
604
+ texts: [],
605
+ attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([path, attributes]) => [target(path), attributes]) : [],
606
+ events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([path, events]) => [target(path), events]) : [],
607
+ expressions: [],
608
+ expressionAttributes: [],
609
+ conditions: [],
610
+ effects: []
611
+ }
612
+ itemParts.set(root, mapped)
613
+ return mapped
614
+ }
478
615
  const target = nested ? ownedElements(root) : [root, ...root.querySelectorAll("*")]
479
- itemParts.set(root, {
616
+ const mapped = {
480
617
  directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
481
618
  texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([index, field]) => [target[index], field]) : [],
482
619
  attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([index, attributes]) => [target[index], attributes]) : [],
@@ -488,19 +625,73 @@ function mapListItemParts(parts, root, nested = false) {
488
625
  return [target[index], descriptor]
489
626
  }) : [],
490
627
  effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(index => target[index]) : []
491
- })
628
+ }
629
+ itemParts.set(root, mapped)
630
+ return mapped
631
+ }
632
+
633
+ function fillStructuralListParts(parts, root, item) {
634
+ for (let partIndex = 0; partIndex < parts.directTexts.length; partIndex++) {
635
+ const part = parts.directTexts[partIndex]
636
+ const path = part[0]
637
+ let node = root
638
+ for (let pathIndex = 0; pathIndex < path.length; pathIndex++) node = node.children[path[pathIndex]]
639
+ node.textContent = item[part[1]] ?? ""
640
+ }
641
+ if (__KUDZU_LIST_ATTRIBUTES__) for (let partIndex = 0; partIndex < parts.attributes.length; partIndex++) {
642
+ const part = parts.attributes[partIndex]
643
+ const path = part[0]
644
+ let node = root
645
+ for (let pathIndex = 0; pathIndex < path.length; pathIndex++) node = node.children[path[pathIndex]]
646
+ const attributes = part[1]
647
+ for (let index = 0; index < attributes.length; index++) patchBinding(node, attributes[index][0], item[attributes[index][1]])
648
+ }
649
+ }
650
+
651
+ function elementPath(root, node) {
652
+ const path = []
653
+ while (node !== root) {
654
+ const parent = node.parentElement
655
+ path.push(Array.prototype.indexOf.call(parent.children, node))
656
+ node = parent
657
+ }
658
+ return path.reverse()
492
659
  }
493
660
 
494
661
  function mapListConditionTemplates(planned, initial) {
495
- if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
496
- for (let index = 0; index < initial.length; index++) {
497
- const marker = initial[index][0]
498
- initial[index][1] ??= planned[index][1]
499
- if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
662
+ if (!__KUDZU_DEEP_LIST_CONDITIONS__) {
663
+ if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
664
+ for (let index = 0; index < initial.length; index++) {
665
+ const marker = initial[index][0]
666
+ initial[index][1] ??= planned[index][1]
667
+ if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
668
+ }
669
+ return
670
+ }
671
+ const templates = new Map()
672
+ const collect = root => {
673
+ for (const marker of root.querySelectorAll("template")) {
674
+ if (marker.hasAttribute("data-k-list-condition")) {
675
+ const descriptor = JSON.parse(marker.dataset.kListCondition)
676
+ templates.set(descriptor.handler, [descriptor, marker])
677
+ }
678
+ collect(marker.content)
679
+ }
680
+ }
681
+ for (const entry of planned) {
682
+ templates.set(entry[1].handler, [entry[1], entry[2]])
683
+ collect(entry[2].content)
684
+ }
685
+ for (const entry of initial) {
686
+ const marker = entry[0]
687
+ const template = templates.get(entry[1]?.handler ?? marker.dataset.kListConditionHandler)
688
+ if (!template) throw new Error("Keyed list condition marker has no matching template")
689
+ entry[1] ??= template[0]
690
+ if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, template[1])
500
691
  }
501
692
  }
502
693
 
503
- function updateListCondition(marker, kind, value, item) {
694
+ function updateListCondition(marker, kind, value, item, index) {
504
695
  const current = listConditionKey(kind, value)
505
696
  if (marker.dataset.kListCurrent === current) return
506
697
  let end = marker.nextSibling
@@ -523,7 +714,7 @@ function updateListCondition(marker, kind, value, item) {
523
714
  const nodes = [...fragment.childNodes]
524
715
  const revision = (revisions.get(marker) ?? 0) + 1
525
716
  revisions.set(marker, revision)
526
- fillListParts(marker, listItemParts(fragment), item, revision)
717
+ fillListParts(marker, listItemParts(fragment), item, revision, index)
527
718
  end.parentNode.insertBefore(fragment, end)
528
719
  marker.dataset.kListCurrent = current
529
720
  const owner = conditionOwners.get(marker)
@@ -539,19 +730,20 @@ function renderFalsy(value) {
539
730
  return value === false || value == null || value === true ? "" : String(value)
540
731
  }
541
732
 
542
- function validateChildLists(items, child) {
733
+ function validateChildLists(items, children) {
543
734
  for (const item of items) {
544
- const children = item?.[child.field]
545
- if (!Array.isArray(children)) throw new Error(`Nested keyed list property "${child.field}" must remain an array`)
546
- const keys = new Set()
547
- for (const entry of children) {
548
- const key = entry?.[child.key]
549
- if (!validListKey(key)) throw new Error(`Keyed list key "${child.key}" must be a string or finite number`)
550
- assertListItem(entry)
551
- assertListValue(entry, new Set(), true)
552
- const token = keyToken(key)
553
- if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
554
- keys.add(token)
735
+ for (const child of children) {
736
+ const values = __KUDZU_COLLECTION_SELECTORS__ ? selectCollection(item?.[child.field], child.selector) : item?.[child.field] ?? []
737
+ const keys = new Set()
738
+ for (const entry of values) {
739
+ const key = child.key === null ? keys.size : entry?.[child.key]
740
+ if (!validListKey(key)) throw new Error(`Keyed list key "${child.key}" must be a string or finite number`)
741
+ assertListItem(entry)
742
+ assertListValue(entry, new Set(), true)
743
+ const token = keyToken(key)
744
+ if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
745
+ keys.add(token)
746
+ }
555
747
  }
556
748
  }
557
749
  }
@@ -564,24 +756,24 @@ function listOwner(start) {
564
756
 
565
757
  function mountNestedPrototype(start, descriptor, roots) {
566
758
  const owner = descriptor.ownerField ? listOwner(start) : undefined
567
- const prototypeStart = owner ? childPrototypes.get(owner) : undefined
759
+ const prototypeStart = owner ? childPrototypes.get(owner)?.get(descriptor.id) : undefined
568
760
  const templateRoot = prototypeStart?.content.firstElementChild ?? start.content.firstElementChild
569
761
  if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
570
- const childPrototype = descriptor.child ? findChildPrototype(templateRoot, descriptor.child.field) : undefined
571
- if (descriptor.child && !childPrototype) throw new Error("Keyed list template has no nested row prototype")
572
- if (childPrototype) for (const root of roots) childPrototypes.set(root, childPrototype)
762
+ const prototypes = descriptor.children && new Map(descriptor.children.map(child => [child.id, findChildPrototype(templateRoot, child.id)]))
763
+ if (prototypes && [...prototypes.values()].some(prototype => !prototype)) throw new Error("Keyed list template has no nested row prototype")
764
+ if (prototypes?.size) for (const root of roots) childPrototypes.set(root, prototypes)
573
765
  if (prototypeStart && start !== prototypeStart) start.content.replaceChildren()
574
- return { owner, childPrototype, templateRoot }
766
+ return { owner, childPrototypes: prototypes, templateRoot }
575
767
  }
576
768
 
577
- function findChildPrototype(root, field) {
769
+ function findChildPrototype(root, id) {
578
770
  for (const element of root.children) {
579
771
  if (element.matches("template[data-k-list]")) {
580
- if (JSON.parse(element.dataset.kList).ownerField === field) return element
581
- const nested = findChildPrototype(element.content, field)
772
+ if (JSON.parse(element.dataset.kList).id === id) return element
773
+ const nested = findChildPrototype(element.content, id)
582
774
  if (nested) return nested
583
775
  } else {
584
- const nested = findChildPrototype(element, field)
776
+ const nested = findChildPrototype(element, id)
585
777
  if (nested) return nested
586
778
  }
587
779
  }
@@ -653,14 +845,14 @@ function patchListText(marker, endSelector, value) {
653
845
  end.before(marker.ownerDocument.createTextNode(text))
654
846
  }
655
847
 
656
- function evaluate(descriptor, item) {
848
+ function evaluate(descriptor, item, index) {
657
849
  let module = imports.get(descriptor.module)
658
850
  if (!module) {
659
851
  module = import(descriptor.module)
660
852
  imports.set(descriptor.module, module)
661
853
  }
662
854
  return module.then(exports => {
663
- const value = exports[descriptor.handler](item)
855
+ const value = exports[descriptor.handler](item, index)
664
856
  if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
665
857
  return value
666
858
  })
@@ -688,7 +880,8 @@ function patchBinding(node, target, value) {
688
880
  } else if (value == null || (value === false && !isStringBooleanAttribute(target))) {
689
881
  node.removeAttribute(target)
690
882
  } else {
691
- node.setAttribute(target, value === true && !isStringBooleanAttribute(target) ? "" : String(value))
883
+ const next = value === true && !isStringBooleanAttribute(target) ? "" : String(value)
884
+ if (node.getAttribute(target) !== next) node.setAttribute(target, next)
692
885
  }
693
886
  }
694
887
 
@@ -696,27 +889,56 @@ function keyToken(key) {
696
889
  return `${typeof key}:${key}`
697
890
  }
698
891
 
892
+ /* general-row-hooks */
893
+ function initializeGeneralRowHooks(descriptor, key, root, owner) {
894
+ const token = keyToken(key)
895
+ const path = [...(ownershipPaths.get(owner) ?? []), `${descriptor.id}=${token}`]
896
+ ownershipPaths.set(root, path)
897
+ const statePath = descriptor.ownerField ? path : [token]
898
+ root.dataset.kRowPath = encodeURIComponent(statePath.join("/"))
899
+ const replacements = new Map()
900
+ for (const state of descriptor.rowStates ?? []) {
901
+ const id = rowStateId(state.id, statePath)
902
+ if (!browserState.has(id)) browserState.set(id, __KUDZU_COMPLEX_LIST_ROW_STATE__ && state.initialValue !== null && typeof state.initialValue === "object" ? structuredClone(state.initialValue) : state.initialValue)
903
+ replacements.set(state.id, id)
904
+ }
905
+ if (__KUDZU_LIST_ROW_REFS__) for (const ref of descriptor.rowRefs ?? []) replacements.set(ref, rowStateId(ref, statePath))
906
+ for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, statePath))
907
+ rowReplacements.set(root, replacements)
908
+ replaceRowIds(root, replacements)
909
+ }
910
+ /* general-row-hooks-end */
911
+
699
912
  function initializeRowStates(descriptor, key, root) {
700
913
  const token = keyToken(key)
701
914
  const replacements = new Map()
702
915
  for (const state of descriptor.rowStates) {
703
- const id = rowStateId(state.id, token)
916
+ const id = flatRowStateId(state.id, token)
704
917
  if (!browserState.has(id)) browserState.set(id, state.initialValue)
705
918
  replacements.set(state.id, id)
706
919
  }
707
- for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, token))
920
+ for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, flatRowStateId(marker, token))
708
921
  if (root) replaceRowIds(root, replacements)
709
922
  }
710
923
 
711
- function deleteRowStates(descriptor, token) {
712
- for (const state of descriptor.rowStates) browserState.delete(rowStateId(state.id, token))
924
+ function deleteFlatRowStates(descriptor, token) {
925
+ for (const state of descriptor.rowStates) browserState.delete(flatRowStateId(state.id, token))
713
926
  }
714
927
 
715
- function rowStateId(id, token) {
928
+ function flatRowStateId(id, token) {
716
929
  return id.replace("$k", encodeURIComponent(token))
717
930
  }
718
931
 
932
+ function deleteRowStates(descriptor, path) {
933
+ const statePath = descriptor.ownerField ? path : [path.at(-1).slice(path.at(-1).indexOf("=") + 1)]
934
+ for (const state of descriptor.rowStates) browserState.delete(rowStateId(state.id, statePath))
935
+ }
936
+ function rowStateId(id, path) {
937
+ return id.replace("$k", encodeURIComponent(path.join("/")))
938
+ }
939
+
719
940
  function replaceRowIds(root, replacements) {
941
+ if (!replacements) return
720
942
  const replace = node => {
721
943
  for (const attribute of [...node.attributes]) {
722
944
  if (!attribute.name.startsWith("data-k-")) continue
@@ -739,7 +961,7 @@ function assertListItem(item) {
739
961
  if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
740
962
  }
741
963
 
742
- function seededListValue(item, fields, seed) {
964
+ function seededListValue(item, fields, seed, serialize = true) {
743
965
  const keys = Reflect.ownKeys(item)
744
966
  if (fields.length === 1) {
745
967
  const field = fields[0]
@@ -749,10 +971,10 @@ function seededListValue(item, fields, seed) {
749
971
  if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
750
972
  const value = descriptor.value
751
973
  const type = value === null ? "null" : typeof value
752
- return type === seed[field] && !(type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) ? value : undefined
974
+ return type === seed[field] && !(type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) ? serialize ? value : true : undefined
753
975
  }
754
976
  if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
755
- const values = []
977
+ const values = serialize ? [] : undefined
756
978
  for (const field of fields) {
757
979
  const descriptor = Object.getOwnPropertyDescriptor(item, field)
758
980
  if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
@@ -760,9 +982,13 @@ function seededListValue(item, fields, seed) {
760
982
  const value = descriptor.value
761
983
  const type = value === null ? "null" : typeof value
762
984
  if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
763
- values.push(value)
985
+ if (serialize) values.push(value)
764
986
  }
765
- return values.length === 1 ? values[0] : JSON.stringify(values)
987
+ return serialize ? values.length === 1 ? values[0] : JSON.stringify(values) : true
988
+ }
989
+
990
+ function usesItemReferences(list) {
991
+ return !list.descriptor.mount && !list.descriptor.effects && !list.descriptor.events && !list.descriptor.expressions && !list.descriptor.expressionAttributes && !list.descriptor.conditions
766
992
  }
767
993
 
768
994
  function assertListValue(value, seen, root = false) {