nexus-shared 3.1.0 → 3.2.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +704 -446
  2. package/LICENSE +21 -21
  3. package/README.md +44 -5
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Forms/Crud.js +322 -161
  7. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  8. package/dist/Components/Forms/CrudHistory.js +140 -0
  9. package/dist/Components/Forms/CrudParts.js +2 -1
  10. package/dist/Components/Forms/Form.js +4 -1
  11. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  12. package/dist/Components/Inputs/CodeBox.js +316 -0
  13. package/dist/Components/Inputs/DatePicker.js +4 -1
  14. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  15. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  16. package/dist/Components/Inputs/Dropdown.js +4 -1
  17. package/dist/Components/Inputs/InputRenderer.js +3 -0
  18. package/dist/Components/Inputs/NumberBox.js +4 -2
  19. package/dist/Components/Inputs/TextArea.js +4 -1
  20. package/dist/Components/Inputs/TextBox.js +4 -1
  21. package/dist/Components/Inputs/TimePicker.js +4 -1
  22. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  23. package/dist/Components/Inputs/TreeView.js +372 -23
  24. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  25. package/dist/Components/Layouts/ThemePicker.js +16 -4
  26. package/dist/Components/Viewers/DataTable.js +63 -21
  27. package/dist/Components/Viewers/DataTableParts.d.ts +26 -8
  28. package/dist/Components/Viewers/DataTableParts.js +39 -10
  29. package/dist/Helpers/ApiModules.d.ts +3 -34
  30. package/dist/Helpers/ApiModules.js +3 -3
  31. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  32. package/dist/Helpers/ApiRoutes.js +31 -27
  33. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  34. package/dist/Helpers/ArrayHelpers.js +2 -0
  35. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  36. package/dist/Helpers/CodeHelpers.js +332 -0
  37. package/dist/Helpers/CrudBackend.d.ts +94 -4
  38. package/dist/Helpers/CrudBackend.js +168 -9
  39. package/dist/Helpers/CrudColumns.d.ts +51 -0
  40. package/dist/Helpers/CrudColumns.js +69 -0
  41. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  42. package/dist/Helpers/CrudHelpers.js +109 -12
  43. package/dist/Helpers/InputParamsHelpers.js +8 -0
  44. package/dist/Helpers/MessageBuilder.js +2 -0
  45. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  46. package/dist/Helpers/NexusHelpers.js +1 -0
  47. package/dist/Helpers/RequestCache.d.ts +35 -0
  48. package/dist/Helpers/RequestCache.js +113 -0
  49. package/dist/Helpers/TableExport.js +12 -12
  50. package/dist/Helpers/TableHelpers.d.ts +1 -1
  51. package/dist/Helpers/TableHelpers.js +7 -2
  52. package/dist/Helpers/TableJoins.d.ts +70 -0
  53. package/dist/Helpers/TableJoins.js +250 -0
  54. package/dist/Helpers/TableStore.d.ts +5 -0
  55. package/dist/Helpers/TableStore.js +9 -0
  56. package/dist/Helpers/TreeStore.d.ts +225 -11
  57. package/dist/Helpers/TreeStore.js +683 -90
  58. package/dist/Helpers/UserPreferences.d.ts +8 -0
  59. package/dist/Helpers/UserPreferences.js +17 -0
  60. package/dist/Interfaces/CrudInterfaces.d.ts +47 -9
  61. package/dist/Interfaces/FormInterfaces.d.ts +4 -1
  62. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  63. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  64. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  65. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  66. package/dist/Interfaces/TableInterfaces.d.ts +86 -4
  67. package/dist/Services/ApiProxy.d.ts +36 -1
  68. package/dist/Services/ApiProxy.js +182 -52
  69. package/dist/Services/ApiProxyRules.d.ts +203 -0
  70. package/dist/Services/ApiProxyRules.js +426 -0
  71. package/dist/Services/JoinSources.d.ts +48 -0
  72. package/dist/Services/JoinSources.js +74 -0
  73. package/dist/Services/ServerApi.d.ts +13 -2
  74. package/dist/Services/ServerApi.js +8 -3
  75. package/dist/Shared.Index.d.ts +4 -0
  76. package/dist/Shared.Index.js +4 -0
  77. package/package.json +87 -85
  78. package/src/Styles/Nexus.Avatar.css +79 -79
  79. package/src/Styles/Nexus.Base.css +117 -117
  80. package/src/Styles/Nexus.Button.css +180 -180
  81. package/src/Styles/Nexus.Calendar.css +273 -273
  82. package/src/Styles/Nexus.Chat.css +1443 -1443
  83. package/src/Styles/Nexus.Choice.css +343 -343
  84. package/src/Styles/Nexus.Crud.css +168 -168
  85. package/src/Styles/Nexus.DateTime.css +81 -81
  86. package/src/Styles/Nexus.Dropdown.css +725 -725
  87. package/src/Styles/Nexus.Editor.css +295 -295
  88. package/src/Styles/Nexus.Excel.css +389 -389
  89. package/src/Styles/Nexus.Form.css +279 -279
  90. package/src/Styles/Nexus.Index.css +37 -37
  91. package/src/Styles/Nexus.Input.css +196 -4
  92. package/src/Styles/Nexus.Markdown.css +237 -237
  93. package/src/Styles/Nexus.Menu.css +237 -237
  94. package/src/Styles/Nexus.Popup.css +724 -724
  95. package/src/Styles/Nexus.Rows.css +637 -637
  96. package/src/Styles/Nexus.Slider.css +276 -276
  97. package/src/Styles/Nexus.Tab.Buttons.css +432 -432
  98. package/src/Styles/Nexus.Table.css +31 -30
  99. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  100. package/src/Styles/Nexus.Themes.css +194 -194
  101. package/src/Styles/Nexus.Time.css +144 -144
  102. package/src/Styles/Nexus.Toast.css +263 -263
  103. package/src/Styles/Nexus.Tokens.css +89 -89
  104. package/src/Styles/Nexus.Tree.css +500 -431
@@ -5,7 +5,55 @@
5
5
  *
6
6
  * Checking a box, the frequent change, updates counts along one path. Adding, renaming, and removing items count
7
7
  * the whole tree again, which takes a few milliseconds at 100,000 items.
8
+ *
9
+ * With `lazy`, children come later, a page at a time, from whoever loads them (TreeView's `loadChildren`): a node with
10
+ * none loaded yet can still be expanded, a page joins its parent with counts updated along one path, and `rows` holds
11
+ * a status row under a parent whose page is on its way, failed, or has more to come. The store never loads anything
12
+ * itself: the view asks, and tells the store what came back (`beginLoad`, `receivePage`, `failLoad`).
13
+ */
14
+ import { RequestCache } from "./RequestCache.js";
15
+ /** Children's pages kept for every lazy tree on the page that names a `cacheKey`, for 5 minutes: see `RequestCache`. */
16
+ export const treePages = new RequestCache();
17
+ /** The start of every key a tree keeps its pages under, or one parent's pages (`null`: the top level). */
18
+ export function treePagesKey(cacheKey, parent) {
19
+ if (parent === undefined)
20
+ return `${cacheKey}\u0000`;
21
+ return `${cacheKey}\u0000${treeKey(parent) ?? ""}\u0000`;
22
+ }
23
+ /** The key one page is kept under. */
24
+ export function treePageKey(cacheKey, parent, skip, take) {
25
+ return `${treePagesKey(cacheKey, parent)}${skip}\u0000${take}`;
26
+ }
27
+ /**
28
+ * Drops the kept pages of a tree's source - all of them, one parent's (`null`: the top level), or several parents' -
29
+ * after the records behind them changed elsewhere. Returns how many were dropped. A tree on screen keeps what it shows:
30
+ * its `refresh()` loads that again.
8
31
  */
32
+ export function expireTreePages(cacheKey, parent) {
33
+ if (!Array.isArray(parent))
34
+ return treePages.drop(treePagesKey(cacheKey, parent));
35
+ const base = treePagesKey(cacheKey);
36
+ const parents = new Set(parent.map(value => treeKey(value) ?? ""));
37
+ return treePages.drop(key => key.startsWith(base) && parents.has(key.slice(base.length, key.indexOf("\u0000", base.length))));
38
+ }
39
+ /** A has-children field read as the tree reads it: a boolean, or a count above 0; `undefined` when it says nothing. */
40
+ export function treeHasChildren(value) {
41
+ if (typeof value === "boolean")
42
+ return value;
43
+ if (typeof value === "number")
44
+ return Number.isFinite(value) ? value > 0 : undefined;
45
+ if (typeof value === "string" && value.trim() !== "") {
46
+ const count = Number(value);
47
+ return Number.isFinite(count) ? count > 0 : undefined;
48
+ }
49
+ return undefined;
50
+ }
51
+ /** The key of a status row: never an item's, since a value's key is never empty and never starts with NUL. */
52
+ const STATUS_KEY = "\u0000";
53
+ /** The key of the status row under a parent (`null`: the top level), as `TreeNode.key` holds it. */
54
+ export function treeStatusKey(parentKey) {
55
+ return STATUS_KEY + (parentKey ?? "");
56
+ }
9
57
  export const TREE_DEFAULT_KEYS = { value: "id", parent: "parentId", label: "label" };
10
58
  const read = (item, key) => item[key];
11
59
  const defaultDisabled = (item) => read(item, "disabled") === true || read(item, "isDisabled") === true;
@@ -16,7 +64,6 @@ export function treeKey(value) {
16
64
  return null;
17
65
  return typeof value === "string" || typeof value === "number" ? String(value) : null;
18
66
  }
19
- let nextUid = 1;
20
67
  /**
21
68
  * A tree built from a flat list. Items whose parent is missing become top-level items, and a loop of parents is
22
69
  * broken at the first item of the loop. Siblings keep the order of the list; a repeated value keeps its first item.
@@ -27,12 +74,31 @@ export class TreeStore {
27
74
  labelKey;
28
75
  cascade;
29
76
  valueMode;
77
+ /** Children come a page at a time from a loader. */
78
+ lazy;
79
+ hasChildrenKey;
30
80
  isDisabled;
31
81
  isChecked;
32
82
  roots = [];
33
- /** The visible rows in order: top-level items, and the children of every expanded item whose parents are expanded. A new array after each change. */
83
+ /**
84
+ * The visible rows in order: top-level items, and the children of every expanded item whose parents are expanded,
85
+ * with a lazy parent's status row after its children. A new array after each change.
86
+ */
34
87
  rows = [];
88
+ /** With `lazyRoots`: where the top level's pages stand. `null` when the top level comes from the items. */
89
+ rootPages = null;
35
90
  byKey = new Map();
91
+ /** The status row of each lazy parent that has had one, by the parent's key ("" for the top level). */
92
+ statusRows = new Map();
93
+ /**
94
+ * With `lazy`: checked values whose items are not loaded yet, or not settled (see `settle`). They stay in the value,
95
+ * and check their items when the items come, so a saved value is never lost to a page not opened yet.
96
+ */
97
+ pending = new Map();
98
+ /** Whether the checks came from a value rather than from the items' own flags; items that come later follow the same rule. */
99
+ valueGiven = false;
100
+ requests = 0;
101
+ nextUid = 1;
36
102
  /** Without `cascade`: checked nodes, nodes that are not locked, and the checked ones among them. */
37
103
  checkedNodes = 0;
38
104
  openNodes = 0;
@@ -44,8 +110,12 @@ export class TreeStore {
44
110
  this.labelKey = options.labelKey ?? TREE_DEFAULT_KEYS.label;
45
111
  this.cascade = options.cascade ?? true;
46
112
  this.valueMode = options.valueMode ?? "leaves";
113
+ this.lazy = options.lazy ?? false;
114
+ this.hasChildrenKey = options.hasChildrenKey;
47
115
  this.isDisabled = options.isItemDisabled ?? defaultDisabled;
48
116
  this.isChecked = options.isItemChecked ?? defaultChecked;
117
+ if (this.lazy && options.lazyRoots)
118
+ this.rootPages = idlePages();
49
119
  this.setItems(items, value);
50
120
  }
51
121
  /** Items in the tree, drafts included. */
@@ -57,9 +127,24 @@ export class TreeStore {
57
127
  const key = treeKey(value);
58
128
  return key === null ? undefined : this.byKey.get(key);
59
129
  }
130
+ /** The node or status row whose `key` this is: what a view keeps for its focused row. */
131
+ find(key) {
132
+ const node = this.byKey.get(key);
133
+ if (node)
134
+ return node;
135
+ if (!key.startsWith(STATUS_KEY))
136
+ return undefined;
137
+ const row = this.statusRows.get(key.slice(STATUS_KEY.length));
138
+ return row && (row.parent === null || this.byKey.get(row.parent.key) === row.parent) ? row : undefined;
139
+ }
140
+ /** Where a lazy parent's children stand (`null`: the top level); `undefined` when they do not come from a loader. */
141
+ pagesOf(parent) {
142
+ return parent ? parent.pages : (this.rootPages ?? undefined);
143
+ }
60
144
  /**
61
145
  * Replaces the items. Expanded items stay expanded. The checked items come from `value` when given, otherwise from
62
- * the items' own flags.
146
+ * the items' own flags. In a lazy tree, an item whose children were loaded keeps them, unless the items now give it
147
+ * children of their own.
63
148
  */
64
149
  setItems(items, value) {
65
150
  const previous = this.byKey;
@@ -102,6 +187,18 @@ export class TreeStore {
102
187
  reached.add(cut);
103
188
  }
104
189
  }
190
+ if (this.lazy) {
191
+ for (const node of ordered) {
192
+ // Children given in the items are all of them.
193
+ if (node.children.length > 0) {
194
+ node.pages = loadedPages();
195
+ continue;
196
+ }
197
+ const before = previous.get(node.key);
198
+ if (before?.pages)
199
+ this.adoptChildren(node, before, byKey);
200
+ }
201
+ }
105
202
  this.byKey = byKey;
106
203
  this.roots = roots;
107
204
  const checked = value ? this.keysOf(value) : null;
@@ -110,8 +207,25 @@ export class TreeStore {
110
207
  node.depth = node.parent ? node.parent.depth + 1 : 0;
111
208
  node.checked = checked ? checked.has(node.key) || this.checkedByParent(node, checked) : this.isChecked(node.item);
112
209
  }
210
+ this.valueGiven = value !== undefined;
211
+ this.pending = this.lazy && value ? valueMap(value) : new Map();
113
212
  this.recount(all);
114
- this.rows = visibleRows(roots);
213
+ this.settleAll();
214
+ this.rows = this.visibleRows(roots, null);
215
+ }
216
+ /** Moves the loaded children (and pages) of a node that is being built again onto its new node. */
217
+ adoptChildren(node, before, byKey) {
218
+ node.pages = before.pages;
219
+ for (const child of before.children) {
220
+ const moved = preOrder([child]);
221
+ // A child whose value an item now uses stays behind, with everything under it.
222
+ if (moved.some(each => byKey.has(each.key)))
223
+ continue;
224
+ child.parent = node;
225
+ node.children.push(child);
226
+ for (const each of moved)
227
+ byKey.set(each.key, each);
228
+ }
115
229
  }
116
230
  /** Every item in tree order, without drafts. */
117
231
  items() {
@@ -131,10 +245,15 @@ export class TreeStore {
131
245
  return list;
132
246
  }
133
247
  // Checking
134
- /** The state of a node's box. */
248
+ /**
249
+ * The state of a node's box. With `cascade`, a lazy parent whose items are not all loaded is never `checked` - its
250
+ * unloaded items are not in the value - unless the value names it (`valueMode: "checked"`, where it stands for all).
251
+ */
135
252
  state(node) {
136
253
  if (!this.cascade || node.children.length === 0)
137
254
  return node.checked ? "checked" : "unchecked";
255
+ if (node.partial)
256
+ return this.pending.has(node.key) ? "checked" : node.checkedLeaves > 0 ? "mixed" : "unchecked";
138
257
  if (node.checkedLeaves === 0)
139
258
  return "unchecked";
140
259
  return node.checkedLeaves === node.leaves ? "checked" : "mixed";
@@ -151,15 +270,37 @@ export class TreeStore {
151
270
  }
152
271
  return { checked, total };
153
272
  }
154
- /** The checked values, in tree order, following `valueMode`. */
273
+ /**
274
+ * The checked values, in tree order, following `valueMode`. In a lazy tree, values whose items are not loaded yet
275
+ * (or not settled) follow the others, so a saved value survives until its items come.
276
+ */
155
277
  value() {
156
- return this.checkedInOrder().map(node => node.value);
278
+ const list = this.checkedInOrder().map(node => node.value);
279
+ if (this.pending.size === 0)
280
+ return list;
281
+ const listed = new Set(list.map(String));
282
+ for (const [key, value] of this.pending)
283
+ if (!listed.has(key))
284
+ list.push(value);
285
+ return list;
157
286
  }
158
- /** The items of the value, in the same order. */
287
+ /** The items of the value, in the same order. In a lazy tree it holds only the items loaded, so it can be shorter than `value()`. */
159
288
  valueItems() {
160
- return this.checkedInOrder().map(node => node.item);
289
+ const nodes = this.checkedInOrder();
290
+ if (this.pending.size === 0)
291
+ return nodes.map(node => node.item);
292
+ const listed = new Set(nodes.map(node => node.key));
293
+ for (const key of this.pending.keys()) {
294
+ const node = this.byKey.get(key);
295
+ if (node && !listed.has(key))
296
+ nodes.push(node);
297
+ }
298
+ return nodes.map(node => node.item);
161
299
  }
162
- /** Checks exactly these values. Locks do not apply: this is the saved state, not a user's change. Returns whether anything changed. */
300
+ /**
301
+ * Checks exactly these values. Locks do not apply: this is the saved state, not a user's change. In a lazy tree a
302
+ * value whose item is not loaded is kept, and checks the item when it comes. Returns whether anything changed.
303
+ */
163
304
  setValue(values) {
164
305
  const keys = this.keysOf(values);
165
306
  let changed = false;
@@ -173,26 +314,44 @@ export class TreeStore {
173
314
  changed = true;
174
315
  }
175
316
  }
317
+ this.valueGiven = true;
176
318
  if (changed)
177
319
  this.recount(all);
178
- return changed;
320
+ if (!this.lazy)
321
+ return changed;
322
+ const before = [...this.pending.keys()].join(STATUS_KEY);
323
+ this.pending = valueMap(values);
324
+ this.settleAll();
325
+ return changed || [...this.pending.keys()].join(STATUS_KEY) !== before;
179
326
  }
180
327
  /**
181
328
  * What a click on the node's box does. With `cascade` it checks every item under the node that is not locked, or
182
329
  * unchecks them when they are all checked already. Returns whether anything changed; nothing does on a locked node.
330
+ *
331
+ * In a lazy tree with `cascade`, a node whose items are not all loaded (`partial`) does not change either: a check
332
+ * stands for everything under it, and the value can only hold the items that were loaded, so checking it would
333
+ * silently leave the rest out. The items under it whose own items are all loaded can still be checked one by one.
183
334
  */
184
335
  toggle(node) {
185
- if (node.locked)
336
+ if (node.locked || node.status)
186
337
  return false;
187
338
  if (!this.cascade)
188
339
  return this.setNodeChecked(node, !node.checked);
189
- return this.setLeaves(node, node.openChecked < node.openLeaves);
340
+ return this.setChecked(node, node.openChecked < node.openLeaves);
190
341
  }
191
342
  /** Checks or unchecks a node; with `cascade`, every item under it that is not locked. Returns whether anything changed. */
192
343
  setChecked(node, checked) {
193
- if (node.locked)
344
+ if (node.locked || node.status)
194
345
  return false;
195
- return this.cascade ? this.setLeaves(node, checked) : this.setNodeChecked(node, checked);
346
+ if (!this.cascade)
347
+ return this.setNodeChecked(node, checked);
348
+ if (node.partial)
349
+ return false;
350
+ const changed = this.setLeaves(node, checked);
351
+ // A parent whose value stood for everything under it ("checked" mode) no longer does once something under it is unchecked.
352
+ if (changed && !checked)
353
+ this.dropClaimsAbove(node);
354
+ return changed;
196
355
  }
197
356
  /** The state of a "select all" box, over the items that are not locked; `null` when every item is locked. */
198
357
  allState() {
@@ -215,12 +374,16 @@ export class TreeStore {
215
374
  const state = this.allState();
216
375
  return state !== null && this.checkAll(state !== "checked");
217
376
  }
218
- /** Checks or unchecks every item that is not locked. Returns whether anything changed. */
377
+ /**
378
+ * Checks or unchecks every item that is not locked. Returns whether anything changed. In a lazy tree with `cascade`,
379
+ * a node whose items are not all loaded is passed over and the loaded parts under it are changed; values whose
380
+ * items are not loaded stay as they are.
381
+ */
219
382
  checkAll(checked) {
220
383
  let changed = false;
221
384
  if (this.cascade) {
222
- for (const root of this.roots)
223
- changed = this.setChecked(root, checked) || changed;
385
+ for (const top of this.settledTops())
386
+ changed = this.setChecked(top, checked) || changed;
224
387
  }
225
388
  else {
226
389
  for (const node of preOrder(this.roots))
@@ -231,11 +394,22 @@ export class TreeStore {
231
394
  // Expanding
232
395
  /** Whether every parent of the node is expanded, so the node is a row. */
233
396
  isVisible(node) {
397
+ if (node.status)
398
+ return this.indexOf(node) >= 0;
234
399
  for (let parent = node.parent; parent; parent = parent.parent)
235
400
  if (!parent.expanded)
236
401
  return false;
237
402
  return this.byKey.get(node.key) === node;
238
403
  }
404
+ /** Whether the node can be expanded: it has children, or (lazy) it may have some not loaded yet. */
405
+ canExpand(node) {
406
+ if (node.status)
407
+ return false;
408
+ if (node.children.length > 0)
409
+ return true;
410
+ const pages = node.pages;
411
+ return pages !== undefined && (pages.status !== "ready" || pages.hasMore);
412
+ }
239
413
  /** The node's row index, or -1 when it is not a row. */
240
414
  indexOf(node) {
241
415
  if (this.indexCache?.rows !== this.rows) {
@@ -245,15 +419,15 @@ export class TreeStore {
245
419
  }
246
420
  return this.indexCache.map.get(node) ?? -1;
247
421
  }
248
- /** Expands a node. Returns the rows it added, or `null` when no rows changed. */
422
+ /** Expands a node. Returns the rows it added (a lazy node's status row too), or `null` when no rows changed. */
249
423
  expand(node) {
250
- if (node.expanded || node.children.length === 0)
424
+ if (node.expanded || !this.canExpand(node))
251
425
  return null;
252
426
  node.expanded = true;
253
427
  const index = this.isVisible(node) ? this.indexOf(node) : -1;
254
428
  if (index < 0)
255
429
  return null;
256
- const added = visibleRows(node.children);
430
+ const added = this.visibleRows(node.children, node);
257
431
  const before = this.rows;
258
432
  this.rows = before.slice(0, index + 1).concat(added, before.slice(index + 1));
259
433
  return { at: index + 1, count: added.length, before };
@@ -275,18 +449,22 @@ export class TreeStore {
275
449
  this.rows = before.slice(0, index + 1).concat(before.slice(end));
276
450
  return { at: index + 1, count: end - index - 1, before };
277
451
  }
278
- /** Expands every item, or only the first `levels` levels (1 expands the top-level items). */
452
+ /**
453
+ * Expands every item, or only the first `levels` levels (1 expands the top-level items). In a lazy tree it opens the
454
+ * items whose children are loaded, and asks for nothing: a node open on a page still on its way stays open.
455
+ */
279
456
  expandAll(levels = Infinity) {
280
- for (const node of this.byKey.values())
281
- node.expanded = node.children.length > 0 && node.depth < levels;
282
- this.rows = visibleRows(this.roots);
457
+ for (const node of this.byKey.values()) {
458
+ node.expanded = node.depth < levels && (node.children.length > 0 || (node.expanded && this.canExpand(node)));
459
+ }
460
+ this.rows = this.visibleRows(this.roots, null);
283
461
  }
284
462
  collapseAll() {
285
463
  for (const node of this.byKey.values())
286
464
  node.expanded = false;
287
- this.rows = visibleRows(this.roots);
465
+ this.rows = this.visibleRows(this.roots, null);
288
466
  }
289
- /** Expands these items and their parents, so the items and their children show. */
467
+ /** Expands these items and their parents, so the items and their children show. In a lazy tree, only items whose children are loaded. */
290
468
  expandValues(values) {
291
469
  for (const key of this.keysOf(values)) {
292
470
  const node = this.byKey.get(key);
@@ -297,7 +475,7 @@ export class TreeStore {
297
475
  for (const parent of this.path(node))
298
476
  parent.expanded = true;
299
477
  }
300
- this.rows = visibleRows(this.roots);
478
+ this.rows = this.visibleRows(this.roots, null);
301
479
  }
302
480
  /** The values of the expanded items, in tree order. */
303
481
  expandedValues() {
@@ -312,9 +490,122 @@ export class TreeStore {
312
490
  return false;
313
491
  for (const parent of closed)
314
492
  parent.expanded = true;
315
- this.rows = visibleRows(this.roots);
493
+ this.rows = this.visibleRows(this.roots, null);
494
+ return true;
495
+ }
496
+ // Loading children (lazy)
497
+ /**
498
+ * Marks a parent's next page as on its way (`null`: the top level) and returns the request's number, which the
499
+ * answer is given back with. An earlier request still out is forgotten. Returns 0 when the parent has no pages.
500
+ */
501
+ beginLoad(parent) {
502
+ const pages = this.pagesOf(parent);
503
+ if (!pages || !this.owns(parent))
504
+ return 0;
505
+ pages.status = "loading";
506
+ pages.request = ++this.requests;
507
+ this.changedPages(parent);
508
+ return pages.request;
509
+ }
510
+ /**
511
+ * Adds a page of children that `request` asked for, after the ones loaded, and works out what follows: `hasMore`
512
+ * when the page says, else whether `total` is reached, else whether the page was full (`take` items). Their parent
513
+ * field is not read: they go under `parent`. A value already in the tree is passed over, so a page that overlaps
514
+ * the last one adds no row twice. Returns the nodes added and the rows they added (for an animation), or `null` when
515
+ * the answer is to an older request or the parent is gone.
516
+ */
517
+ receivePage(parent, request, page, take) {
518
+ const pages = this.pagesOf(parent);
519
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
520
+ return null;
521
+ const items = Array.isArray(page?.items) ? page.items : [];
522
+ // A first page leaves nothing old behind it.
523
+ if (pages.loaded === 0)
524
+ pages.stale = false;
525
+ pages.loaded += items.length;
526
+ if (typeof page.total === "number" && Number.isFinite(page.total))
527
+ pages.total = page.total;
528
+ pages.hasMore = morePages(pages, page, items.length, take);
529
+ pages.status = "ready";
530
+ const before = this.rows;
531
+ const added = this.addNodes(parent, items);
532
+ this.settleAround(parent, added);
533
+ // A parent that turned out to have no children closes: its chevron goes with its status row.
534
+ if (parent && parent.children.length === 0 && !this.canExpand(parent))
535
+ this.collapse(parent);
536
+ else
537
+ this.refreshBlock(parent);
538
+ const at = added.length > 0 && this.rows !== before ? this.indexOf(added[0]) : -1;
539
+ return { added, change: at >= 0 ? { at, count: added.length, before } : null };
540
+ }
541
+ /** The page `request` asked for failed: the parent shows a failed row with Retry. Returns false for an older request. */
542
+ failLoad(parent, request) {
543
+ const pages = this.pagesOf(parent);
544
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
545
+ return false;
546
+ pages.status = "failed";
547
+ this.changedPages(parent);
316
548
  return true;
317
549
  }
550
+ /**
551
+ * Replaces a parent's children with fresh pages after a refresh (`request` from `beginLoad`): an item still there
552
+ * keeps its node - what is expanded and loaded under it - and takes the fresh item; one no longer there goes, and a
553
+ * value it held waits in the value for its item to come back. Returns false for an older request.
554
+ */
555
+ replacePages(parent, request, answers, take) {
556
+ const pages = this.pagesOf(parent);
557
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
558
+ return false;
559
+ const items = answers.flatMap(page => (Array.isArray(page?.items) ? [...page.items] : []));
560
+ const last = answers[answers.length - 1] ?? { items: [] };
561
+ pages.loaded = items.length;
562
+ pages.total = undefined;
563
+ for (const page of answers)
564
+ if (typeof page.total === "number" && Number.isFinite(page.total))
565
+ pages.total = page.total;
566
+ pages.hasMore = morePages(pages, last, Array.isArray(last.items) ? last.items.length : 0, take);
567
+ pages.status = "ready";
568
+ pages.stale = false;
569
+ const valueBefore = this.value();
570
+ this.reconcile(parent, items);
571
+ this.recount(preOrder(this.roots));
572
+ const valueAfter = new Set(this.value().map(String));
573
+ for (const value of valueBefore)
574
+ if (!valueAfter.has(String(value)))
575
+ this.pending.set(String(value), value);
576
+ this.settleAll();
577
+ this.rows = this.visibleRows(this.roots, null);
578
+ return true;
579
+ }
580
+ /**
581
+ * Marks loaded pages old, for a refresh: every lazy parent (`null`), or a node and everything under it. The view
582
+ * loads again the ones on screen, and the rest load when next expanded; a page on its way is forgotten, and a node
583
+ * that had answered with no children may have some now.
584
+ */
585
+ markStale(node = null) {
586
+ if (!this.lazy)
587
+ return;
588
+ if (node === null && this.rootPages)
589
+ this.rootPages = this.stalePages(this.rootPages, null);
590
+ for (const each of node ? preOrder([node]) : preOrder(this.roots))
591
+ if (each.pages)
592
+ each.pages = this.stalePages(each.pages, each);
593
+ this.recount(preOrder(this.roots));
594
+ this.rows = this.visibleRows(this.roots, null);
595
+ }
596
+ stalePages(pages, owner) {
597
+ if (pages.status === "loading") {
598
+ pages.request = ++this.requests;
599
+ pages.status = pages.loaded > 0 ? "ready" : "idle";
600
+ }
601
+ if (pages.status === "idle")
602
+ return pages;
603
+ // Nothing loaded under it: it starts again from what its item says.
604
+ if (owner && owner.children.length === 0)
605
+ return this.newPages(owner.item);
606
+ pages.stale = true;
607
+ return pages;
608
+ }
318
609
  // Changing items
319
610
  /**
320
611
  * Adds an item under `parent` (at the top level for `null`), as its last child unless `index` is given, and expands
@@ -330,6 +621,9 @@ export class TreeStore {
330
621
  node.draft = options.draft ?? false;
331
622
  node.parent = parent;
332
623
  node.depth = parent ? parent.depth + 1 : 0;
624
+ // A new item has no children anywhere yet, so a lazy tree has nothing to ask about it.
625
+ if (this.lazy)
626
+ node.pages = loadedPages();
333
627
  // An item added under a checked leaf starts checked, so its parent stays checked.
334
628
  node.checked = Boolean(this.cascade && parent && parent.children.length === 0 && parent.checked);
335
629
  const siblings = parent ? parent.children : this.roots;
@@ -338,9 +632,22 @@ export class TreeStore {
338
632
  if (parent)
339
633
  parent.expanded = true;
340
634
  this.recount(preOrder(this.roots));
341
- this.rows = visibleRows(this.roots);
635
+ this.rows = this.visibleRows(this.roots, null);
342
636
  return node;
343
637
  }
638
+ /**
639
+ * Adds many items under `parent` (at the top level for `null`) after its children, in one pass: counts are updated
640
+ * along one path, not over the whole tree, so a page of 50 joins a tree of 100,000 in well under a millisecond. A
641
+ * value already in the tree is passed over. The parent is not expanded. Returns the nodes added.
642
+ */
643
+ insertMany(parent, items) {
644
+ if (parent && this.byKey.get(parent.key) !== parent)
645
+ return [];
646
+ const added = this.addNodes(parent, items);
647
+ this.settleAround(parent, added);
648
+ this.refreshBlock(parent);
649
+ return added;
650
+ }
344
651
  /**
345
652
  * Replaces a node's item, e.g. after a rename, or once a draft is saved and the server gave it an id. The value can
346
653
  * change; the parent cannot. Throws on a value another item uses.
@@ -377,19 +684,23 @@ export class TreeStore {
377
684
  if (parent && this.cascade && siblings.length === 1)
378
685
  parent.checked = this.state(parent) === "checked";
379
686
  siblings.splice(index, 1);
380
- if (parent && parent.children.length === 0)
687
+ if (parent && parent.children.length === 0 && !this.canExpand(parent))
381
688
  parent.expanded = false;
382
- for (const item of removed)
689
+ for (const item of removed) {
383
690
  this.byKey.delete(item.key);
691
+ this.statusRows.delete(item.key);
692
+ // A deleted item's value goes with it.
693
+ this.pending.delete(item.key);
694
+ }
384
695
  node.parent = null;
385
696
  this.recount(preOrder(this.roots));
386
- this.rows = visibleRows(this.roots);
697
+ this.rows = this.visibleRows(this.roots, null);
387
698
  return removed;
388
699
  }
389
700
  // Internals
390
701
  createNode(item, key) {
391
702
  return {
392
- uid: nextUid++,
703
+ uid: this.nextUid++,
393
704
  key,
394
705
  value: read(item, this.valueKey),
395
706
  item,
@@ -408,8 +719,266 @@ export class TreeStore {
408
719
  openLeaves: 0,
409
720
  openChecked: 0,
410
721
  draft: false,
722
+ pages: this.lazy ? this.newPages(item) : undefined,
723
+ partial: false,
411
724
  };
412
725
  }
726
+ /** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
727
+ newPages(item) {
728
+ const known = this.hasChildrenKey === undefined ? undefined : treeHasChildren(read(item, this.hasChildrenKey));
729
+ return known === false ? loadedPages() : idlePages();
730
+ }
731
+ /** Whether a parent (`null`: the top level) is still in the tree, so an answer for it applies. */
732
+ owns(parent) {
733
+ return parent === null || this.byKey.get(parent.key) === parent;
734
+ }
735
+ /** After a parent's pages changed state: its `partial` and counts along its path, and its rows. */
736
+ changedPages(parent) {
737
+ if (parent) {
738
+ const path = this.path(parent).concat(parent);
739
+ this.count(path, -1);
740
+ for (let index = path.length - 1; index >= 0; index--)
741
+ this.aggregate(path[index]);
742
+ this.count(path, 1);
743
+ }
744
+ this.refreshBlock(parent);
745
+ }
746
+ /**
747
+ * Creates nodes for items under a parent, after its children, and updates counts along the parent's path only. A
748
+ * value already in the tree is passed over.
749
+ */
750
+ addNodes(parent, items) {
751
+ const path = parent ? this.path(parent).concat(parent) : [];
752
+ this.count(path, -1);
753
+ const siblings = parent ? parent.children : this.roots;
754
+ const added = [];
755
+ for (const item of items) {
756
+ const key = treeKey(read(item, this.valueKey));
757
+ if (key === null || this.byKey.has(key))
758
+ continue;
759
+ const node = this.createNode(item, key);
760
+ node.parent = parent;
761
+ node.depth = parent ? parent.depth + 1 : 0;
762
+ node.position = siblings.length;
763
+ node.disabledPath = node.disabled || Boolean(parent?.disabledPath);
764
+ node.checked = this.arrivalChecked(node);
765
+ siblings.push(node);
766
+ this.byKey.set(key, node);
767
+ this.aggregate(node);
768
+ added.push(node);
769
+ }
770
+ for (let index = path.length - 1; index >= 0; index--)
771
+ this.aggregate(path[index]);
772
+ this.count(path, 1);
773
+ this.count(added, 1);
774
+ return added;
775
+ }
776
+ /** Adds (1) or takes away (-1) nodes' share of the counts kept for a tree without `cascade`. */
777
+ count(nodes, step) {
778
+ for (const node of nodes) {
779
+ if (node.checked)
780
+ this.checkedNodes += step;
781
+ if (node.locked)
782
+ continue;
783
+ this.openNodes += step;
784
+ if (node.checked)
785
+ this.openCheckedNodes += step;
786
+ }
787
+ }
788
+ /** Whether an item that has just come starts checked: its value is waiting, a parent's value stands for it, or its own flag. */
789
+ arrivalChecked(node) {
790
+ if (this.pending.has(node.key))
791
+ return true;
792
+ if (this.cascade && this.valueMode === "checked" && this.pending.size > 0) {
793
+ for (let parent = node.parent; parent; parent = parent.parent)
794
+ if (this.pending.has(parent.key))
795
+ return true;
796
+ }
797
+ return !this.valueGiven && this.isChecked(node.item);
798
+ }
799
+ /**
800
+ * Whether a waiting value is settled now that its item is here, so the tree's own checks say it all:
801
+ * - without `cascade`, at once - the item is its own value, and came checked;
802
+ * - with `cascade`, a parent named in a value of leaves (or of every parent with a checked item) means nothing, as in
803
+ * a tree loaded whole;
804
+ * - with `cascade`, any other waits until everything under its item is loaded: an unloaded item, or a "checked"-mode
805
+ * parent that stands for all of its items, loaded or not.
806
+ */
807
+ settle(node) {
808
+ if (!this.pending.has(node.key))
809
+ return;
810
+ if (!this.cascade || (node.children.length > 0 && this.valueMode !== "checked") || !node.partial)
811
+ this.pending.delete(node.key);
812
+ }
813
+ settleAll() {
814
+ if (this.pending.size === 0)
815
+ return;
816
+ for (const key of [...this.pending.keys()]) {
817
+ const node = this.byKey.get(key);
818
+ if (node)
819
+ this.settle(node);
820
+ }
821
+ }
822
+ settleAround(parent, added) {
823
+ if (this.pending.size === 0)
824
+ return;
825
+ for (const node of added)
826
+ this.settle(node);
827
+ for (let node = parent; node; node = node.parent)
828
+ this.settle(node);
829
+ }
830
+ dropClaimsAbove(node) {
831
+ if (this.pending.size === 0)
832
+ return;
833
+ for (let parent = node.parent; parent; parent = parent.parent)
834
+ this.pending.delete(parent.key);
835
+ }
836
+ /** With `cascade`: the highest nodes whose items are all loaded - the roots, or below a partial node its loaded parts. */
837
+ settledTops() {
838
+ const list = [];
839
+ const stack = [...this.roots].reverse();
840
+ while (stack.length > 0) {
841
+ const node = stack.pop();
842
+ if (!node.partial)
843
+ list.push(node);
844
+ else
845
+ for (let index = node.children.length - 1; index >= 0; index--)
846
+ stack.push(node.children[index]);
847
+ }
848
+ return list;
849
+ }
850
+ /**
851
+ * Fresh items in place of a parent's children: a node whose value is still there is kept (with what is expanded and
852
+ * loaded under it) and takes the fresh item; one not there goes, unless it is a draft being named.
853
+ */
854
+ reconcile(parent, items) {
855
+ const siblings = parent ? parent.children : this.roots;
856
+ const old = new Map(siblings.map(node => [node.key, node]));
857
+ const next = [];
858
+ for (const item of items) {
859
+ const key = treeKey(read(item, this.valueKey));
860
+ if (key === null)
861
+ continue;
862
+ const kept = old.get(key);
863
+ if (kept) {
864
+ old.delete(key);
865
+ kept.item = item;
866
+ kept.value = read(item, this.valueKey);
867
+ kept.label = labelOf(item, this.labelKey);
868
+ kept.disabled = this.isDisabled(item);
869
+ // Nothing loaded under it: what the fresh item says about its children counts.
870
+ if (kept.children.length === 0 && kept.pages && kept.pages.status !== "loading")
871
+ kept.pages = this.newPages(item);
872
+ next.push(kept);
873
+ }
874
+ else if (!this.byKey.has(key)) {
875
+ const node = this.createNode(item, key);
876
+ node.parent = parent;
877
+ node.depth = parent ? parent.depth + 1 : 0;
878
+ node.checked = this.arrivalChecked(node);
879
+ this.byKey.set(key, node);
880
+ next.push(node);
881
+ }
882
+ }
883
+ for (const gone of old.values()) {
884
+ if (gone.draft) {
885
+ next.push(gone);
886
+ continue;
887
+ }
888
+ for (const each of preOrder([gone])) {
889
+ this.byKey.delete(each.key);
890
+ this.statusRows.delete(each.key);
891
+ }
892
+ gone.parent = null;
893
+ }
894
+ siblings.splice(0, siblings.length, ...next);
895
+ }
896
+ /** Rows again for a parent's block (`null`: every row), when it is expanded and a row: its children's rows and its status row. */
897
+ refreshBlock(parent) {
898
+ const before = this.rows;
899
+ let start = 0;
900
+ let end = before.length;
901
+ if (parent) {
902
+ if (!parent.expanded)
903
+ return;
904
+ const index = this.isVisible(parent) ? this.indexOf(parent) : -1;
905
+ if (index < 0)
906
+ return;
907
+ start = index + 1;
908
+ end = start;
909
+ while (end < before.length && before[end].depth > parent.depth)
910
+ end++;
911
+ }
912
+ this.rows = before.slice(0, start).concat(this.visibleRows(parent ? parent.children : this.roots, parent), before.slice(end));
913
+ }
914
+ /** The rows these nodes show - each node, then the rows of its children when it is expanded - and, in a lazy tree, each expanded parent's status row after its children. `parent` owns `nodes`, for its own status row. */
915
+ visibleRows(nodes, parent) {
916
+ const list = [];
917
+ const stack = [];
918
+ const own = this.statusRowOf(parent);
919
+ if (own)
920
+ stack.push(own);
921
+ for (let index = nodes.length - 1; index >= 0; index--)
922
+ stack.push(nodes[index]);
923
+ while (stack.length > 0) {
924
+ const node = stack.pop();
925
+ list.push(node);
926
+ if (!node.expanded)
927
+ continue;
928
+ if (this.lazy) {
929
+ const status = this.statusRowOf(node);
930
+ if (status)
931
+ stack.push(status);
932
+ }
933
+ for (let index = node.children.length - 1; index >= 0; index--)
934
+ stack.push(node.children[index]);
935
+ }
936
+ return list;
937
+ }
938
+ /** The status row an expanded parent (`null`: the top level) shows after its children, if its pages need one. */
939
+ statusRowOf(parent) {
940
+ if (!this.lazy)
941
+ return undefined;
942
+ const pages = this.pagesOf(parent);
943
+ if (!pages)
944
+ return undefined;
945
+ // An open parent not asked yet is about to be: it shows as loading.
946
+ const kind = pages.status === "loading" || pages.status === "idle" ? "loading" : pages.status === "failed" ? "error" : pages.hasMore ? "more" : null;
947
+ if (kind === null)
948
+ return undefined;
949
+ const parentKey = parent ? parent.key : "";
950
+ let row = this.statusRows.get(parentKey);
951
+ if (!row || row.parent !== parent) {
952
+ // Not built by createNode: the item callbacks (isItemDisabled) are never handed an empty object.
953
+ row = {
954
+ uid: this.nextUid++,
955
+ key: STATUS_KEY + parentKey,
956
+ value: "",
957
+ item: EMPTY_ITEM,
958
+ label: "",
959
+ parent,
960
+ children: [],
961
+ depth: 0,
962
+ position: 0,
963
+ disabled: false,
964
+ disabledPath: false,
965
+ locked: false,
966
+ expanded: false,
967
+ checked: false,
968
+ leaves: 0,
969
+ checkedLeaves: 0,
970
+ openLeaves: 0,
971
+ openChecked: 0,
972
+ draft: false,
973
+ partial: false,
974
+ };
975
+ this.statusRows.set(parentKey, row);
976
+ }
977
+ row.status = kind;
978
+ row.depth = parent ? parent.depth + 1 : 0;
979
+ row.disabledPath = Boolean(parent?.disabledPath);
980
+ return row;
981
+ }
413
982
  keysOf(values) {
414
983
  const keys = new Set();
415
984
  for (const value of values) {
@@ -434,51 +1003,53 @@ export class TreeStore {
434
1003
  for (const node of ordered) {
435
1004
  node.children.forEach((child, index) => (child.position = index));
436
1005
  node.disabledPath = node.disabled || Boolean(node.parent?.disabledPath);
1006
+ }
1007
+ for (let index = ordered.length - 1; index >= 0; index--)
1008
+ this.aggregate(ordered[index]);
1009
+ this.checkedNodes = 0;
1010
+ this.openNodes = 0;
1011
+ this.openCheckedNodes = 0;
1012
+ this.count(ordered, 1);
1013
+ }
1014
+ /**
1015
+ * A node's lock, `partial`, and counts, from its own flags and its children's (already worked out): what `recount`
1016
+ * does for every node, and what a page of children does for each node on its parent's path.
1017
+ */
1018
+ aggregate(node) {
1019
+ const pages = node.pages;
1020
+ const own = pages !== undefined && (pages.status !== "ready" || pages.hasMore);
1021
+ if (node.children.length === 0) {
1022
+ node.partial = own;
437
1023
  node.locked = node.disabledPath;
1024
+ // A lazy node not loaded yet is a leaf whose box cannot be changed with cascade: it shows what the value says.
1025
+ const fixed = node.locked || (this.cascade && own);
1026
+ node.leaves = 1;
1027
+ node.checkedLeaves = node.checked ? 1 : 0;
1028
+ node.openLeaves = fixed ? 0 : 1;
1029
+ node.openChecked = fixed ? 0 : node.checkedLeaves;
1030
+ return;
438
1031
  }
439
- let checkedNodes = 0;
440
- let openNodes = 0;
441
- let openCheckedNodes = 0;
442
- for (let index = ordered.length - 1; index >= 0; index--) {
443
- const node = ordered[index];
444
- if (node.children.length === 0) {
445
- node.leaves = 1;
446
- node.checkedLeaves = node.checked ? 1 : 0;
447
- node.openLeaves = node.locked ? 0 : 1;
448
- node.openChecked = node.locked ? 0 : node.checkedLeaves;
449
- }
450
- else {
451
- let leaves = 0;
452
- let checked = 0;
453
- let openLeaves = 0;
454
- let openChecked = 0;
455
- let allLocked = true;
456
- for (const child of node.children) {
457
- leaves += child.leaves;
458
- checked += child.checkedLeaves;
459
- openLeaves += child.openLeaves;
460
- openChecked += child.openChecked;
461
- allLocked &&= child.locked;
462
- }
463
- node.leaves = leaves;
464
- node.checkedLeaves = checked;
465
- node.openLeaves = openLeaves;
466
- node.openChecked = openChecked;
467
- // A parent's box follows its items only with cascade; without it, a parent stays free.
468
- if (this.cascade)
469
- node.locked ||= allLocked;
470
- }
471
- if (node.checked)
472
- checkedNodes++;
473
- if (!node.locked) {
474
- openNodes++;
475
- if (node.checked)
476
- openCheckedNodes++;
477
- }
1032
+ let leaves = 0;
1033
+ let checked = 0;
1034
+ let openLeaves = 0;
1035
+ let openChecked = 0;
1036
+ let allLocked = true;
1037
+ let partial = own;
1038
+ for (const child of node.children) {
1039
+ leaves += child.leaves;
1040
+ checked += child.checkedLeaves;
1041
+ openLeaves += child.openLeaves;
1042
+ openChecked += child.openChecked;
1043
+ allLocked &&= child.locked;
1044
+ partial ||= child.partial;
478
1045
  }
479
- this.checkedNodes = checkedNodes;
480
- this.openNodes = openNodes;
481
- this.openCheckedNodes = openCheckedNodes;
1046
+ node.leaves = leaves;
1047
+ node.checkedLeaves = checked;
1048
+ node.openLeaves = openLeaves;
1049
+ node.openChecked = openChecked;
1050
+ node.partial = partial;
1051
+ // A parent's box follows its items only with cascade; without it, a parent stays free.
1052
+ node.locked = node.disabledPath || (this.cascade && allLocked);
482
1053
  }
483
1054
  /** Sets every open leaf under a node (the node itself for a leaf), updating counts up the path of each leaf that changes. */
484
1055
  setLeaves(node, checked) {
@@ -537,8 +1108,15 @@ export class TreeStore {
537
1108
  // A subtree without a checked leaf adds nothing.
538
1109
  if (node.checkedLeaves === 0)
539
1110
  continue;
540
- if (node.children.length === 0 || mode === "any" || (mode === "checked" && node.checkedLeaves === node.leaves))
1111
+ if (node.partial) {
1112
+ // Not everything under it is loaded: it is in the value only when the value names it (see `value`), except
1113
+ // as a parent with a checked item in "any" mode, which it is.
1114
+ if (mode === "any" && node.children.length > 0)
1115
+ list.push(node);
1116
+ }
1117
+ else if (node.children.length === 0 || mode === "any" || (mode === "checked" && node.checkedLeaves === node.leaves)) {
541
1118
  list.push(node);
1119
+ }
542
1120
  }
543
1121
  for (let index = node.children.length - 1; index >= 0; index--)
544
1122
  stack.push(node.children[index]);
@@ -546,6 +1124,34 @@ export class TreeStore {
546
1124
  return list;
547
1125
  }
548
1126
  }
1127
+ /** Placeholder item of a status row. */
1128
+ const EMPTY_ITEM = Object.freeze({});
1129
+ function idlePages() {
1130
+ return { status: "idle", loaded: 0, hasMore: true, stale: false, request: 0 };
1131
+ }
1132
+ /** Pages of a node whose children are all known: given in the items, created here, or none at all. */
1133
+ function loadedPages() {
1134
+ return { status: "ready", loaded: 0, hasMore: false, stale: false, request: 0 };
1135
+ }
1136
+ /** Whether another page follows the one that came with `count` items. A page with nothing in it ends the list, whatever it says. */
1137
+ function morePages(pages, page, count, take) {
1138
+ if (count === 0)
1139
+ return false;
1140
+ if (typeof page.hasMore === "boolean")
1141
+ return page.hasMore;
1142
+ if (pages.total !== undefined)
1143
+ return pages.loaded < pages.total;
1144
+ return count >= take;
1145
+ }
1146
+ function valueMap(values) {
1147
+ const map = new Map();
1148
+ for (const value of values) {
1149
+ const key = treeKey(value);
1150
+ if (key !== null && !map.has(key))
1151
+ map.set(key, value);
1152
+ }
1153
+ return map;
1154
+ }
549
1155
  /** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
550
1156
  export function preOrder(nodes) {
551
1157
  const list = [];
@@ -558,19 +1164,6 @@ export function preOrder(nodes) {
558
1164
  }
559
1165
  return list;
560
1166
  }
561
- /** The rows these nodes show: each node, then the rows of its children when it is expanded. */
562
- function visibleRows(nodes) {
563
- const list = [];
564
- const stack = [...nodes].reverse();
565
- while (stack.length > 0) {
566
- const node = stack.pop();
567
- list.push(node);
568
- if (node.expanded)
569
- for (let index = node.children.length - 1; index >= 0; index--)
570
- stack.push(node.children[index]);
571
- }
572
- return list;
573
- }
574
1167
  function labelOf(item, key) {
575
1168
  const label = read(item, key);
576
1169
  return label === null || label === undefined ? "" : String(label);