nexus-shared 3.1.0 → 4.0.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 (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -5,7 +5,261 @@
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.
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
+ /**
40
+ * Writes changes made elsewhere into the pages kept for a tree's source, and into every tree on screen that keeps its
41
+ * pages under `cacheKey` - so a record saved in a form, moved, or sent to the trash shows in the tree at once, and
42
+ * leaving the page and coming back reads nothing again for it. Items go where the parent's children are known (see
43
+ * `TreeStore.placeItem`); a parent whose children were never loaded only counts one more or one fewer, and its first page
44
+ * comes from the server when it is opened, as it would have anyway.
8
45
  */
46
+ export function writeTreeChanges(cacheKey, changes, rules = {}) {
47
+ if (changes.length === 0)
48
+ return;
49
+ patchTreePages(treePages, cacheKey, changes, rules);
50
+ for (const listener of [...(treeListeners.get(cacheKey) ?? [])])
51
+ listener(changes, rules);
52
+ }
53
+ const treeListeners = new Map();
54
+ /** Hears the changes written for `cacheKey`: what a tree on screen does to write them into itself. Returns the way out. */
55
+ export function subscribeTreeChanges(cacheKey, listener) {
56
+ let set = treeListeners.get(cacheKey);
57
+ if (!set)
58
+ treeListeners.set(cacheKey, (set = new Set()));
59
+ set.add(listener);
60
+ return () => {
61
+ set.delete(listener);
62
+ if (set.size === 0 && treeListeners.get(cacheKey) === set)
63
+ treeListeners.delete(cacheKey);
64
+ };
65
+ }
66
+ /**
67
+ * The kept pages under `cacheKey` with the changes written in (what `writeTreeChanges` does to `treePages`). A parent's
68
+ * pages are read as one list from its first child, the changes applied to it, and the list written back in pages of the
69
+ * same size, each with the parent's total - so a tree reading them again, page by page, asks the server for exactly the
70
+ * children it does not have. A page still on its way for a changed parent is dropped: its answer may predate the change.
71
+ */
72
+ export function patchTreePages(cache, cacheKey, changes, rules = {}) {
73
+ const valueKey = rules.valueKey ?? TREE_DEFAULT_KEYS.value;
74
+ const keyOf = (item) => treeKey(read(item, valueKey));
75
+ const groups = keptChildren(cache, cacheKey);
76
+ const of = (parent) => groups.filter(group => group.parent === parent);
77
+ /** A parent's count, on its own item, wherever that item is kept. */
78
+ const count = (parent, delta) => {
79
+ if (parent === "" || rules.hasChildrenKey === undefined)
80
+ return;
81
+ const field = rules.hasChildrenKey;
82
+ const known = of(parent).find(group => group.total !== undefined)?.total;
83
+ for (const group of groups) {
84
+ const index = group.items.findIndex(item => keyOf(item) === parent);
85
+ if (index < 0)
86
+ continue;
87
+ const item = group.items[index];
88
+ const next = countedChildren(read(item, field), delta, known);
89
+ if (next === undefined)
90
+ continue;
91
+ group.items[index] = { ...item, [field]: next };
92
+ group.changed = true;
93
+ }
94
+ };
95
+ for (const change of changes) {
96
+ if (change.type === "put") {
97
+ const key = keyOf(change.item);
98
+ if (key === null)
99
+ continue;
100
+ const parent = treeKey(change.parent) ?? "";
101
+ let from = change.before === false ? false : change.before === undefined ? undefined : (treeKey(change.before) ?? "");
102
+ // Out of the list that holds it, remembering where, for an item that stays where it was.
103
+ const held = new Map();
104
+ for (const group of groups) {
105
+ const index = group.items.findIndex(item => keyOf(item) === key);
106
+ if (index < 0)
107
+ continue;
108
+ from ??= group.parent;
109
+ group.items.splice(index, 1);
110
+ group.changed = true;
111
+ held.set(group, index);
112
+ }
113
+ const moved = typeof from === "string" && from !== parent;
114
+ if (moved) {
115
+ for (const group of of(from)) {
116
+ if (group.total !== undefined)
117
+ group.total = Math.max(0, group.total - 1);
118
+ group.changed = true;
119
+ }
120
+ }
121
+ for (const group of of(parent)) {
122
+ if (!group.listed)
123
+ continue;
124
+ const at = rules.compare ? insertionIndex(group.items, change.item, rules.compare) : Math.min(held.get(group) ?? group.items.length, group.items.length);
125
+ // After the children kept while more are to come: it is in a page not read yet.
126
+ if (at < group.items.length || !group.hasMore)
127
+ group.items.splice(at, 0, change.item);
128
+ if ((moved || from === false) && group.total !== undefined)
129
+ group.total++;
130
+ group.changed = true;
131
+ }
132
+ if (moved)
133
+ count(from, -1);
134
+ if (moved || from === false)
135
+ count(parent, 1);
136
+ }
137
+ else {
138
+ const key = treeKey(change.value);
139
+ if (key === null)
140
+ continue;
141
+ let from = change.parent === undefined ? undefined : (treeKey(change.parent) ?? "");
142
+ let found = false;
143
+ for (const group of groups) {
144
+ const index = group.items.findIndex(item => keyOf(item) === key);
145
+ if (index < 0)
146
+ continue;
147
+ from ??= group.parent;
148
+ found = true;
149
+ group.items.splice(index, 1);
150
+ if (group.total !== undefined)
151
+ group.total = Math.max(0, group.total - 1);
152
+ group.changed = true;
153
+ }
154
+ if (from === undefined)
155
+ continue;
156
+ // Not among the children kept: one of those not read yet, which the total still counted.
157
+ if (!found) {
158
+ for (const group of of(from)) {
159
+ if (group.total !== undefined)
160
+ group.total = Math.max(0, group.total - 1);
161
+ group.changed = true;
162
+ }
163
+ }
164
+ count(from, -1);
165
+ }
166
+ }
167
+ for (const group of groups) {
168
+ if (!group.changed)
169
+ continue;
170
+ const prefix = treePagesKey(cacheKey, group.parent === "" ? null : group.parent);
171
+ const kept = new Set(group.keys);
172
+ cache.drop(key => kept.has(key) || (key.startsWith(prefix) && cache.isLoading(key)));
173
+ // A list emptied while more are to come has nothing to start from: the next open asks the server.
174
+ if (!group.listed || (group.items.length === 0 && group.hasMore))
175
+ continue;
176
+ const hasMore = group.total !== undefined ? group.items.length < group.total : group.hasMore;
177
+ const pages = Math.max(1, Math.ceil(group.items.length / group.take));
178
+ for (let page = 0; page < pages; page++) {
179
+ const items = group.items.slice(page * group.take, (page + 1) * group.take);
180
+ const last = page === pages - 1;
181
+ cache.set(treePageKey(cacheKey, group.parent === "" ? null : group.parent, page * group.take, group.take), { items, total: group.total, hasMore: last ? hasMore : true }, group.at);
182
+ }
183
+ }
184
+ }
185
+ /** The kept pages under `cacheKey`, by parent and page size, each read as one list from the parent's first child. */
186
+ function keptChildren(cache, cacheKey) {
187
+ const base = treePagesKey(cacheKey);
188
+ const found = new Map();
189
+ for (const { key, value, at } of cache.entries(base)) {
190
+ const [parent, skipText, takeText] = key.slice(base.length).split(NUL);
191
+ const skip = Number(skipText);
192
+ const take = Number(takeText);
193
+ if (parent === undefined || !Number.isInteger(skip) || !Number.isInteger(take) || take < 1)
194
+ continue;
195
+ const id = `${parent}${NUL}${take}`;
196
+ let group = found.get(id);
197
+ if (!group)
198
+ found.set(id, (group = { parent, take, pages: [] }));
199
+ group.pages.push({ key, skip, page: value, at });
200
+ }
201
+ const groups = [];
202
+ for (const { parent, take, pages } of found.values()) {
203
+ pages.sort((a, b) => a.skip - b.skip);
204
+ const items = [];
205
+ let total;
206
+ let hasMore = false;
207
+ let at = Infinity;
208
+ for (const { skip, page, at: loadedAt } of pages) {
209
+ // A page that does not follow the last one cannot be placed: the list ends before it.
210
+ if (skip !== items.length)
211
+ break;
212
+ const pageItems = Array.isArray(page?.items) ? page.items : [];
213
+ items.push(...pageItems);
214
+ if (typeof page.total === "number" && Number.isFinite(page.total))
215
+ total = page.total;
216
+ hasMore = pageItems.length === 0 ? false : typeof page.hasMore === "boolean" ? page.hasMore : total !== undefined ? items.length < total : pageItems.length >= take;
217
+ at = Math.min(at, loadedAt);
218
+ if (!hasMore)
219
+ break;
220
+ }
221
+ const listed = pages[0]?.skip === 0;
222
+ groups.push({ parent, take, keys: pages.map(page => page.key), listed, items, total, hasMore, at: Number.isFinite(at) ? at : Math.min(...pages.map(page => page.at)), changed: false });
223
+ }
224
+ return groups;
225
+ }
226
+ /** Where an item goes among siblings in `compare`'s order: before the first that sorts after it. */
227
+ function insertionIndex(items, item, compare) {
228
+ const index = items.findIndex(other => compare(item, other) < 0);
229
+ return index < 0 ? items.length : index;
230
+ }
231
+ /**
232
+ * A parent's has-children field after it gained (1) or lost (-1) a child: a count moves by one, `true` stays true and
233
+ * becomes it, and `false` comes only when the parent's total says none are left. `undefined` when nothing changes.
234
+ */
235
+ function countedChildren(value, delta, total) {
236
+ if (typeof value === "number" && Number.isFinite(value))
237
+ return Math.max(0, value + delta);
238
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)))
239
+ return Math.max(0, Number(value) + delta);
240
+ if (delta > 0)
241
+ return value === true ? undefined : true;
242
+ return value === true && total === 0 ? false : undefined;
243
+ }
244
+ const NUL = "\u0000";
245
+ /** A has-children field read as the tree reads it: a boolean, or a count above 0; `undefined` when it says nothing. */
246
+ export function treeHasChildren(value) {
247
+ if (typeof value === "boolean")
248
+ return value;
249
+ if (typeof value === "number")
250
+ return Number.isFinite(value) ? value > 0 : undefined;
251
+ if (typeof value === "string" && value.trim() !== "") {
252
+ const count = Number(value);
253
+ return Number.isFinite(count) ? count > 0 : undefined;
254
+ }
255
+ return undefined;
256
+ }
257
+ /** The key of a status row: never an item's, since a value's key is never empty and never starts with NUL. */
258
+ const STATUS_KEY = "\u0000";
259
+ /** The key of the status row under a parent (`null`: the top level), as `TreeNode.key` holds it. */
260
+ export function treeStatusKey(parentKey) {
261
+ return STATUS_KEY + (parentKey ?? "");
262
+ }
9
263
  export const TREE_DEFAULT_KEYS = { value: "id", parent: "parentId", label: "label" };
10
264
  const read = (item, key) => item[key];
11
265
  const defaultDisabled = (item) => read(item, "disabled") === true || read(item, "isDisabled") === true;
@@ -16,7 +270,6 @@ export function treeKey(value) {
16
270
  return null;
17
271
  return typeof value === "string" || typeof value === "number" ? String(value) : null;
18
272
  }
19
- let nextUid = 1;
20
273
  /**
21
274
  * A tree built from a flat list. Items whose parent is missing become top-level items, and a loop of parents is
22
275
  * broken at the first item of the loop. Siblings keep the order of the list; a repeated value keeps its first item.
@@ -27,12 +280,31 @@ export class TreeStore {
27
280
  labelKey;
28
281
  cascade;
29
282
  valueMode;
283
+ /** Children come a page at a time from a loader. */
284
+ lazy;
285
+ hasChildrenKey;
30
286
  isDisabled;
31
287
  isChecked;
32
288
  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. */
289
+ /**
290
+ * The visible rows in order: top-level items, and the children of every expanded item whose parents are expanded,
291
+ * with a lazy parent's status row after its children. A new array after each change.
292
+ */
34
293
  rows = [];
294
+ /** With `lazyRoots`: where the top level's pages stand. `null` when the top level comes from the items. */
295
+ rootPages = null;
35
296
  byKey = new Map();
297
+ /** The status row of each lazy parent that has had one, by the parent's key ("" for the top level). */
298
+ statusRows = new Map();
299
+ /**
300
+ * With `lazy`: checked values whose items are not loaded yet, or not settled (see `settle`). They stay in the value,
301
+ * and check their items when the items come, so a saved value is never lost to a page not opened yet.
302
+ */
303
+ pending = new Map();
304
+ /** Whether the checks came from a value rather than from the items' own flags; items that come later follow the same rule. */
305
+ valueGiven = false;
306
+ requests = 0;
307
+ nextUid = 1;
36
308
  /** Without `cascade`: checked nodes, nodes that are not locked, and the checked ones among them. */
37
309
  checkedNodes = 0;
38
310
  openNodes = 0;
@@ -44,8 +316,12 @@ export class TreeStore {
44
316
  this.labelKey = options.labelKey ?? TREE_DEFAULT_KEYS.label;
45
317
  this.cascade = options.cascade ?? true;
46
318
  this.valueMode = options.valueMode ?? "leaves";
319
+ this.lazy = options.lazy ?? false;
320
+ this.hasChildrenKey = options.hasChildrenKey;
47
321
  this.isDisabled = options.isItemDisabled ?? defaultDisabled;
48
322
  this.isChecked = options.isItemChecked ?? defaultChecked;
323
+ if (this.lazy && options.lazyRoots)
324
+ this.rootPages = idlePages();
49
325
  this.setItems(items, value);
50
326
  }
51
327
  /** Items in the tree, drafts included. */
@@ -57,9 +333,24 @@ export class TreeStore {
57
333
  const key = treeKey(value);
58
334
  return key === null ? undefined : this.byKey.get(key);
59
335
  }
336
+ /** The node or status row whose `key` this is: what a view keeps for its focused row. */
337
+ find(key) {
338
+ const node = this.byKey.get(key);
339
+ if (node)
340
+ return node;
341
+ if (!key.startsWith(STATUS_KEY))
342
+ return undefined;
343
+ const row = this.statusRows.get(key.slice(STATUS_KEY.length));
344
+ return row && (row.parent === null || this.byKey.get(row.parent.key) === row.parent) ? row : undefined;
345
+ }
346
+ /** Where a lazy parent's children stand (`null`: the top level); `undefined` when they do not come from a loader. */
347
+ pagesOf(parent) {
348
+ return parent ? parent.pages : (this.rootPages ?? undefined);
349
+ }
60
350
  /**
61
351
  * Replaces the items. Expanded items stay expanded. The checked items come from `value` when given, otherwise from
62
- * the items' own flags.
352
+ * the items' own flags. In a lazy tree, an item whose children were loaded keeps them, unless the items now give it
353
+ * children of their own.
63
354
  */
64
355
  setItems(items, value) {
65
356
  const previous = this.byKey;
@@ -102,6 +393,18 @@ export class TreeStore {
102
393
  reached.add(cut);
103
394
  }
104
395
  }
396
+ if (this.lazy) {
397
+ for (const node of ordered) {
398
+ // Children given in the items are all of them.
399
+ if (node.children.length > 0) {
400
+ node.pages = loadedPages();
401
+ continue;
402
+ }
403
+ const before = previous.get(node.key);
404
+ if (before?.pages)
405
+ this.adoptChildren(node, before, byKey);
406
+ }
407
+ }
105
408
  this.byKey = byKey;
106
409
  this.roots = roots;
107
410
  const checked = value ? this.keysOf(value) : null;
@@ -110,8 +413,25 @@ export class TreeStore {
110
413
  node.depth = node.parent ? node.parent.depth + 1 : 0;
111
414
  node.checked = checked ? checked.has(node.key) || this.checkedByParent(node, checked) : this.isChecked(node.item);
112
415
  }
416
+ this.valueGiven = value !== undefined;
417
+ this.pending = this.lazy && value ? valueMap(value) : new Map();
113
418
  this.recount(all);
114
- this.rows = visibleRows(roots);
419
+ this.settleAll();
420
+ this.rows = this.visibleRows(roots, null);
421
+ }
422
+ /** Moves the loaded children (and pages) of a node that is being built again onto its new node. */
423
+ adoptChildren(node, before, byKey) {
424
+ node.pages = before.pages;
425
+ for (const child of before.children) {
426
+ const moved = preOrder([child]);
427
+ // A child whose value an item now uses stays behind, with everything under it.
428
+ if (moved.some(each => byKey.has(each.key)))
429
+ continue;
430
+ child.parent = node;
431
+ node.children.push(child);
432
+ for (const each of moved)
433
+ byKey.set(each.key, each);
434
+ }
115
435
  }
116
436
  /** Every item in tree order, without drafts. */
117
437
  items() {
@@ -131,10 +451,15 @@ export class TreeStore {
131
451
  return list;
132
452
  }
133
453
  // Checking
134
- /** The state of a node's box. */
454
+ /**
455
+ * The state of a node's box. With `cascade`, a lazy parent whose items are not all loaded is never `checked` - its
456
+ * unloaded items are not in the value - unless the value names it (`valueMode: "checked"`, where it stands for all).
457
+ */
135
458
  state(node) {
136
459
  if (!this.cascade || node.children.length === 0)
137
460
  return node.checked ? "checked" : "unchecked";
461
+ if (node.partial)
462
+ return this.pending.has(node.key) ? "checked" : node.checkedLeaves > 0 ? "mixed" : "unchecked";
138
463
  if (node.checkedLeaves === 0)
139
464
  return "unchecked";
140
465
  return node.checkedLeaves === node.leaves ? "checked" : "mixed";
@@ -151,15 +476,37 @@ export class TreeStore {
151
476
  }
152
477
  return { checked, total };
153
478
  }
154
- /** The checked values, in tree order, following `valueMode`. */
479
+ /**
480
+ * The checked values, in tree order, following `valueMode`. In a lazy tree, values whose items are not loaded yet
481
+ * (or not settled) follow the others, so a saved value survives until its items come.
482
+ */
155
483
  value() {
156
- return this.checkedInOrder().map(node => node.value);
484
+ const list = this.checkedInOrder().map(node => node.value);
485
+ if (this.pending.size === 0)
486
+ return list;
487
+ const listed = new Set(list.map(String));
488
+ for (const [key, value] of this.pending)
489
+ if (!listed.has(key))
490
+ list.push(value);
491
+ return list;
157
492
  }
158
- /** The items of the value, in the same order. */
493
+ /** 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
494
  valueItems() {
160
- return this.checkedInOrder().map(node => node.item);
495
+ const nodes = this.checkedInOrder();
496
+ if (this.pending.size === 0)
497
+ return nodes.map(node => node.item);
498
+ const listed = new Set(nodes.map(node => node.key));
499
+ for (const key of this.pending.keys()) {
500
+ const node = this.byKey.get(key);
501
+ if (node && !listed.has(key))
502
+ nodes.push(node);
503
+ }
504
+ return nodes.map(node => node.item);
161
505
  }
162
- /** Checks exactly these values. Locks do not apply: this is the saved state, not a user's change. Returns whether anything changed. */
506
+ /**
507
+ * Checks exactly these values. Locks do not apply: this is the saved state, not a user's change. In a lazy tree a
508
+ * value whose item is not loaded is kept, and checks the item when it comes. Returns whether anything changed.
509
+ */
163
510
  setValue(values) {
164
511
  const keys = this.keysOf(values);
165
512
  let changed = false;
@@ -173,26 +520,44 @@ export class TreeStore {
173
520
  changed = true;
174
521
  }
175
522
  }
523
+ this.valueGiven = true;
176
524
  if (changed)
177
525
  this.recount(all);
178
- return changed;
526
+ if (!this.lazy)
527
+ return changed;
528
+ const before = [...this.pending.keys()].join(STATUS_KEY);
529
+ this.pending = valueMap(values);
530
+ this.settleAll();
531
+ return changed || [...this.pending.keys()].join(STATUS_KEY) !== before;
179
532
  }
180
533
  /**
181
534
  * What a click on the node's box does. With `cascade` it checks every item under the node that is not locked, or
182
535
  * unchecks them when they are all checked already. Returns whether anything changed; nothing does on a locked node.
536
+ *
537
+ * In a lazy tree with `cascade`, a node whose items are not all loaded (`partial`) does not change either: a check
538
+ * stands for everything under it, and the value can only hold the items that were loaded, so checking it would
539
+ * silently leave the rest out. The items under it whose own items are all loaded can still be checked one by one.
183
540
  */
184
541
  toggle(node) {
185
- if (node.locked)
542
+ if (node.locked || node.status)
186
543
  return false;
187
544
  if (!this.cascade)
188
545
  return this.setNodeChecked(node, !node.checked);
189
- return this.setLeaves(node, node.openChecked < node.openLeaves);
546
+ return this.setChecked(node, node.openChecked < node.openLeaves);
190
547
  }
191
548
  /** Checks or unchecks a node; with `cascade`, every item under it that is not locked. Returns whether anything changed. */
192
549
  setChecked(node, checked) {
193
- if (node.locked)
550
+ if (node.locked || node.status)
551
+ return false;
552
+ if (!this.cascade)
553
+ return this.setNodeChecked(node, checked);
554
+ if (node.partial)
194
555
  return false;
195
- return this.cascade ? this.setLeaves(node, checked) : this.setNodeChecked(node, checked);
556
+ const changed = this.setLeaves(node, checked);
557
+ // A parent whose value stood for everything under it ("checked" mode) no longer does once something under it is unchecked.
558
+ if (changed && !checked)
559
+ this.dropClaimsAbove(node);
560
+ return changed;
196
561
  }
197
562
  /** The state of a "select all" box, over the items that are not locked; `null` when every item is locked. */
198
563
  allState() {
@@ -215,12 +580,16 @@ export class TreeStore {
215
580
  const state = this.allState();
216
581
  return state !== null && this.checkAll(state !== "checked");
217
582
  }
218
- /** Checks or unchecks every item that is not locked. Returns whether anything changed. */
583
+ /**
584
+ * Checks or unchecks every item that is not locked. Returns whether anything changed. In a lazy tree with `cascade`,
585
+ * a node whose items are not all loaded is passed over and the loaded parts under it are changed; values whose
586
+ * items are not loaded stay as they are.
587
+ */
219
588
  checkAll(checked) {
220
589
  let changed = false;
221
590
  if (this.cascade) {
222
- for (const root of this.roots)
223
- changed = this.setChecked(root, checked) || changed;
591
+ for (const top of this.settledTops())
592
+ changed = this.setChecked(top, checked) || changed;
224
593
  }
225
594
  else {
226
595
  for (const node of preOrder(this.roots))
@@ -231,11 +600,22 @@ export class TreeStore {
231
600
  // Expanding
232
601
  /** Whether every parent of the node is expanded, so the node is a row. */
233
602
  isVisible(node) {
603
+ if (node.status)
604
+ return this.indexOf(node) >= 0;
234
605
  for (let parent = node.parent; parent; parent = parent.parent)
235
606
  if (!parent.expanded)
236
607
  return false;
237
608
  return this.byKey.get(node.key) === node;
238
609
  }
610
+ /** Whether the node can be expanded: it has children, or (lazy) it may have some not loaded yet. */
611
+ canExpand(node) {
612
+ if (node.status)
613
+ return false;
614
+ if (node.children.length > 0)
615
+ return true;
616
+ const pages = node.pages;
617
+ return pages !== undefined && (pages.status !== "ready" || pages.hasMore);
618
+ }
239
619
  /** The node's row index, or -1 when it is not a row. */
240
620
  indexOf(node) {
241
621
  if (this.indexCache?.rows !== this.rows) {
@@ -245,15 +625,15 @@ export class TreeStore {
245
625
  }
246
626
  return this.indexCache.map.get(node) ?? -1;
247
627
  }
248
- /** Expands a node. Returns the rows it added, or `null` when no rows changed. */
628
+ /** Expands a node. Returns the rows it added (a lazy node's status row too), or `null` when no rows changed. */
249
629
  expand(node) {
250
- if (node.expanded || node.children.length === 0)
630
+ if (node.expanded || !this.canExpand(node))
251
631
  return null;
252
632
  node.expanded = true;
253
633
  const index = this.isVisible(node) ? this.indexOf(node) : -1;
254
634
  if (index < 0)
255
635
  return null;
256
- const added = visibleRows(node.children);
636
+ const added = this.visibleRows(node.children, node);
257
637
  const before = this.rows;
258
638
  this.rows = before.slice(0, index + 1).concat(added, before.slice(index + 1));
259
639
  return { at: index + 1, count: added.length, before };
@@ -275,18 +655,22 @@ export class TreeStore {
275
655
  this.rows = before.slice(0, index + 1).concat(before.slice(end));
276
656
  return { at: index + 1, count: end - index - 1, before };
277
657
  }
278
- /** Expands every item, or only the first `levels` levels (1 expands the top-level items). */
658
+ /**
659
+ * Expands every item, or only the first `levels` levels (1 expands the top-level items). In a lazy tree it opens the
660
+ * items whose children are loaded, and asks for nothing: a node open on a page still on its way stays open.
661
+ */
279
662
  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);
663
+ for (const node of this.byKey.values()) {
664
+ node.expanded = node.depth < levels && (node.children.length > 0 || (node.expanded && this.canExpand(node)));
665
+ }
666
+ this.rows = this.visibleRows(this.roots, null);
283
667
  }
284
668
  collapseAll() {
285
669
  for (const node of this.byKey.values())
286
670
  node.expanded = false;
287
- this.rows = visibleRows(this.roots);
671
+ this.rows = this.visibleRows(this.roots, null);
288
672
  }
289
- /** Expands these items and their parents, so the items and their children show. */
673
+ /** Expands these items and their parents, so the items and their children show. In a lazy tree, only items whose children are loaded. */
290
674
  expandValues(values) {
291
675
  for (const key of this.keysOf(values)) {
292
676
  const node = this.byKey.get(key);
@@ -297,7 +681,7 @@ export class TreeStore {
297
681
  for (const parent of this.path(node))
298
682
  parent.expanded = true;
299
683
  }
300
- this.rows = visibleRows(this.roots);
684
+ this.rows = this.visibleRows(this.roots, null);
301
685
  }
302
686
  /** The values of the expanded items, in tree order. */
303
687
  expandedValues() {
@@ -312,9 +696,122 @@ export class TreeStore {
312
696
  return false;
313
697
  for (const parent of closed)
314
698
  parent.expanded = true;
315
- this.rows = visibleRows(this.roots);
699
+ this.rows = this.visibleRows(this.roots, null);
700
+ return true;
701
+ }
702
+ // Loading children (lazy)
703
+ /**
704
+ * Marks a parent's next page as on its way (`null`: the top level) and returns the request's number, which the
705
+ * answer is given back with. An earlier request still out is forgotten. Returns 0 when the parent has no pages.
706
+ */
707
+ beginLoad(parent) {
708
+ const pages = this.pagesOf(parent);
709
+ if (!pages || !this.owns(parent))
710
+ return 0;
711
+ pages.status = "loading";
712
+ pages.request = ++this.requests;
713
+ this.changedPages(parent);
714
+ return pages.request;
715
+ }
716
+ /**
717
+ * Adds a page of children that `request` asked for, after the ones loaded, and works out what follows: `hasMore`
718
+ * when the page says, else whether `total` is reached, else whether the page was full (`take` items). Their parent
719
+ * field is not read: they go under `parent`. A value already in the tree is passed over, so a page that overlaps
720
+ * the last one adds no row twice. Returns the nodes added and the rows they added (for an animation), or `null` when
721
+ * the answer is to an older request or the parent is gone.
722
+ */
723
+ receivePage(parent, request, page, take) {
724
+ const pages = this.pagesOf(parent);
725
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
726
+ return null;
727
+ const items = Array.isArray(page?.items) ? page.items : [];
728
+ // A first page leaves nothing old behind it.
729
+ if (pages.loaded === 0)
730
+ pages.stale = false;
731
+ pages.loaded += items.length;
732
+ if (typeof page.total === "number" && Number.isFinite(page.total))
733
+ pages.total = page.total;
734
+ pages.hasMore = morePages(pages, page, items.length, take);
735
+ pages.status = "ready";
736
+ const before = this.rows;
737
+ const added = this.addNodes(parent, items);
738
+ this.settleAround(parent, added);
739
+ // A parent that turned out to have no children closes: its chevron goes with its status row.
740
+ if (parent && parent.children.length === 0 && !this.canExpand(parent))
741
+ this.collapse(parent);
742
+ else
743
+ this.refreshBlock(parent);
744
+ const at = added.length > 0 && this.rows !== before ? this.indexOf(added[0]) : -1;
745
+ return { added, change: at >= 0 ? { at, count: added.length, before } : null };
746
+ }
747
+ /** The page `request` asked for failed: the parent shows a failed row with Retry. Returns false for an older request. */
748
+ failLoad(parent, request) {
749
+ const pages = this.pagesOf(parent);
750
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
751
+ return false;
752
+ pages.status = "failed";
753
+ this.changedPages(parent);
754
+ return true;
755
+ }
756
+ /**
757
+ * Replaces a parent's children with fresh pages after a refresh (`request` from `beginLoad`): an item still there
758
+ * keeps its node - what is expanded and loaded under it - and takes the fresh item; one no longer there goes, and a
759
+ * value it held waits in the value for its item to come back. Returns false for an older request.
760
+ */
761
+ replacePages(parent, request, answers, take) {
762
+ const pages = this.pagesOf(parent);
763
+ if (!pages || pages.request !== request || pages.status !== "loading" || !this.owns(parent))
764
+ return false;
765
+ const items = answers.flatMap(page => (Array.isArray(page?.items) ? [...page.items] : []));
766
+ const last = answers[answers.length - 1] ?? { items: [] };
767
+ pages.loaded = items.length;
768
+ pages.total = undefined;
769
+ for (const page of answers)
770
+ if (typeof page.total === "number" && Number.isFinite(page.total))
771
+ pages.total = page.total;
772
+ pages.hasMore = morePages(pages, last, Array.isArray(last.items) ? last.items.length : 0, take);
773
+ pages.status = "ready";
774
+ pages.stale = false;
775
+ const valueBefore = this.value();
776
+ this.reconcile(parent, items);
777
+ this.recount(preOrder(this.roots));
778
+ const valueAfter = new Set(this.value().map(String));
779
+ for (const value of valueBefore)
780
+ if (!valueAfter.has(String(value)))
781
+ this.pending.set(String(value), value);
782
+ this.settleAll();
783
+ this.rows = this.visibleRows(this.roots, null);
316
784
  return true;
317
785
  }
786
+ /**
787
+ * Marks loaded pages old, for a refresh: every lazy parent (`null`), or a node and everything under it. The view
788
+ * loads again the ones on screen, and the rest load when next expanded; a page on its way is forgotten, and a node
789
+ * that had answered with no children may have some now.
790
+ */
791
+ markStale(node = null) {
792
+ if (!this.lazy)
793
+ return;
794
+ if (node === null && this.rootPages)
795
+ this.rootPages = this.stalePages(this.rootPages, null);
796
+ for (const each of node ? preOrder([node]) : preOrder(this.roots))
797
+ if (each.pages)
798
+ each.pages = this.stalePages(each.pages, each);
799
+ this.recount(preOrder(this.roots));
800
+ this.rows = this.visibleRows(this.roots, null);
801
+ }
802
+ stalePages(pages, owner) {
803
+ if (pages.status === "loading") {
804
+ pages.request = ++this.requests;
805
+ pages.status = pages.loaded > 0 ? "ready" : "idle";
806
+ }
807
+ if (pages.status === "idle")
808
+ return pages;
809
+ // Nothing loaded under it: it starts again from what its item says.
810
+ if (owner && owner.children.length === 0)
811
+ return this.newPages(owner.item);
812
+ pages.stale = true;
813
+ return pages;
814
+ }
318
815
  // Changing items
319
816
  /**
320
817
  * Adds an item under `parent` (at the top level for `null`), as its last child unless `index` is given, and expands
@@ -330,6 +827,9 @@ export class TreeStore {
330
827
  node.draft = options.draft ?? false;
331
828
  node.parent = parent;
332
829
  node.depth = parent ? parent.depth + 1 : 0;
830
+ // A new item has no children anywhere yet, so a lazy tree has nothing to ask about it.
831
+ if (this.lazy)
832
+ node.pages = loadedPages();
333
833
  // An item added under a checked leaf starts checked, so its parent stays checked.
334
834
  node.checked = Boolean(this.cascade && parent && parent.children.length === 0 && parent.checked);
335
835
  const siblings = parent ? parent.children : this.roots;
@@ -338,9 +838,22 @@ export class TreeStore {
338
838
  if (parent)
339
839
  parent.expanded = true;
340
840
  this.recount(preOrder(this.roots));
341
- this.rows = visibleRows(this.roots);
841
+ this.rows = this.visibleRows(this.roots, null);
342
842
  return node;
343
843
  }
844
+ /**
845
+ * Adds many items under `parent` (at the top level for `null`) after its children, in one pass: counts are updated
846
+ * 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
847
+ * value already in the tree is passed over. The parent is not expanded. Returns the nodes added.
848
+ */
849
+ insertMany(parent, items) {
850
+ if (parent && this.byKey.get(parent.key) !== parent)
851
+ return [];
852
+ const added = this.addNodes(parent, items);
853
+ this.settleAround(parent, added);
854
+ this.refreshBlock(parent);
855
+ return added;
856
+ }
344
857
  /**
345
858
  * 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
859
  * change; the parent cannot. Throws on a value another item uses.
@@ -377,19 +890,182 @@ export class TreeStore {
377
890
  if (parent && this.cascade && siblings.length === 1)
378
891
  parent.checked = this.state(parent) === "checked";
379
892
  siblings.splice(index, 1);
380
- if (parent && parent.children.length === 0)
893
+ if (parent && parent.children.length === 0 && !this.canExpand(parent))
381
894
  parent.expanded = false;
382
- for (const item of removed)
895
+ for (const item of removed) {
383
896
  this.byKey.delete(item.key);
897
+ this.statusRows.delete(item.key);
898
+ // A deleted item's value goes with it.
899
+ this.pending.delete(item.key);
900
+ }
384
901
  node.parent = null;
385
902
  this.recount(preOrder(this.roots));
386
- this.rows = visibleRows(this.roots);
903
+ this.rows = this.visibleRows(this.roots, null);
904
+ return removed;
905
+ }
906
+ /**
907
+ * Writes an item where a change made elsewhere left it (see `TreeItemChange`): in place under the same parent (moved
908
+ * among its siblings by `compare`), under its new parent when it moved - with what is loaded under it - and in for the
909
+ * first time when it is new or back. Only where the parent's children are known: given, loaded, or known to be none.
910
+ * Under a parent never opened it waits for that parent's first page, and one that sorts after the loaded children while
911
+ * more are to come waits for its page. A lazy parent's `loaded` and `total` follow, so its next page skips the right
912
+ * number, and so does the count in `hasChildrenKey` on the parent's own item, which its chevron goes by. Returns the
913
+ * item's node where it is now, or `undefined` when this tree does not show it.
914
+ */
915
+ placeItem(item, parentValue, options = {}) {
916
+ const key = treeKey(read(item, this.valueKey));
917
+ if (key === null)
918
+ return undefined;
919
+ const existing = this.byKey.get(key);
920
+ if (existing?.draft)
921
+ return undefined;
922
+ const parentKey = treeKey(parentValue);
923
+ const parent = parentKey === null ? null : this.byKey.get(parentKey);
924
+ // Where it was: its node knows, else the change says - or nobody does, and then no count moves.
925
+ let from;
926
+ if (existing)
927
+ from = existing.parent ? existing.parent.key : null;
928
+ else if (options.before === false)
929
+ from = false;
930
+ else if (options.before !== undefined)
931
+ from = treeKey(options.before);
932
+ const moved = from !== undefined && from !== false && from !== parentKey;
933
+ const valueBefore = this.value();
934
+ const oldParent = existing ? existing.parent : null;
935
+ let oldIndex = -1;
936
+ if (existing) {
937
+ const siblings = oldParent ? oldParent.children : this.roots;
938
+ oldIndex = siblings.indexOf(existing);
939
+ siblings.splice(oldIndex, 1);
940
+ const pages = this.pagesOf(oldParent);
941
+ if (pages && pages.loaded > 0)
942
+ pages.loaded--;
943
+ }
944
+ let node;
945
+ if (parent !== undefined && this.listsChildren(parent)) {
946
+ const siblings = parent ? parent.children : this.roots;
947
+ const pages = this.pagesOf(parent);
948
+ const at = options.compare ? insertionIndex(siblings.map(each => each.item), item, options.compare) : existing && !moved ? Math.min(oldIndex, siblings.length) : siblings.length;
949
+ if (at < siblings.length || !pages?.hasMore) {
950
+ node = existing ?? this.createNode(item, key);
951
+ node.parent = parent;
952
+ siblings.splice(at, 0, node);
953
+ if (pages)
954
+ pages.loaded++;
955
+ if (!existing) {
956
+ node.checked = this.arrivalChecked(node);
957
+ this.byKey.set(key, node);
958
+ }
959
+ }
960
+ }
961
+ if (existing && !node) {
962
+ // It went where this tree has not loaded: out, with everything under it.
963
+ for (const each of preOrder([existing])) {
964
+ this.byKey.delete(each.key);
965
+ this.statusRows.delete(each.key);
966
+ }
967
+ existing.parent = null;
968
+ }
969
+ if (node) {
970
+ node.item = item;
971
+ node.value = read(item, this.valueKey);
972
+ node.label = labelOf(item, this.labelKey);
973
+ node.disabled = this.isDisabled(item);
974
+ // Nothing loaded under it: what the item says about its children counts.
975
+ if (this.lazy && node.children.length === 0 && node.pages && node.pages.status !== "loading")
976
+ node.pages = this.newPages(item);
977
+ for (const each of preOrder([node]))
978
+ each.depth = each.parent ? each.parent.depth + 1 : 0;
979
+ }
980
+ if (moved)
981
+ this.countChild(from, -1);
982
+ if (moved || from === false)
983
+ this.countChild(parentKey, 1);
984
+ // A parent left with no children and nothing more to load closes, and loses its chevron.
985
+ if (oldParent && oldParent.children.length === 0 && !this.canExpand(oldParent))
986
+ oldParent.expanded = false;
987
+ this.recount(preOrder(this.roots));
988
+ // A checked value whose item went out of view waits in the value for it to come back, as after a refresh.
989
+ const valueAfter = new Set(this.value().map(String));
990
+ for (const value of valueBefore)
991
+ if (!valueAfter.has(String(value)))
992
+ this.pending.set(String(value), value);
993
+ this.settleAll();
994
+ this.rows = this.visibleRows(this.roots, null);
995
+ return node;
996
+ }
997
+ /**
998
+ * Takes out an item a change made elsewhere took out of the tree - moved to the trash, deleted - with everything under
999
+ * it. Its parent counts one fewer (its pages' `loaded` and `total`, the count on its item) and loses its chevron when
1000
+ * none are left. An item not loaded here still lowers its parent's counts when `parentValue` names the parent.
1001
+ * Returns the nodes taken out.
1002
+ */
1003
+ removeItem(value, parentValue) {
1004
+ const key = treeKey(value);
1005
+ if (key === null)
1006
+ return [];
1007
+ const node = this.byKey.get(key);
1008
+ if (node?.draft)
1009
+ return [];
1010
+ let removed = [];
1011
+ if (node) {
1012
+ const parent = node.parent;
1013
+ const siblings = parent ? parent.children : this.roots;
1014
+ siblings.splice(siblings.indexOf(node), 1);
1015
+ const pages = this.pagesOf(parent);
1016
+ if (pages && pages.loaded > 0)
1017
+ pages.loaded--;
1018
+ removed = preOrder([node]);
1019
+ for (const each of removed) {
1020
+ this.byKey.delete(each.key);
1021
+ this.statusRows.delete(each.key);
1022
+ this.pending.delete(each.key);
1023
+ }
1024
+ node.parent = null;
1025
+ this.countChild(parent ? parent.key : null, -1);
1026
+ if (parent && parent.children.length === 0 && !this.canExpand(parent))
1027
+ parent.expanded = false;
1028
+ }
1029
+ else if (parentValue !== undefined) {
1030
+ this.countChild(treeKey(parentValue), -1);
1031
+ }
1032
+ else {
1033
+ return [];
1034
+ }
1035
+ this.recount(preOrder(this.roots));
1036
+ this.settleAll();
1037
+ this.rows = this.visibleRows(this.roots, null);
387
1038
  return removed;
388
1039
  }
389
1040
  // Internals
1041
+ /** Whether a parent's children are known here: given in the items, loaded (all or the first pages), or known to be none. */
1042
+ listsChildren(parent) {
1043
+ const pages = this.pagesOf(parent);
1044
+ return !pages || pages.loaded > 0 || pages.status === "ready";
1045
+ }
1046
+ /**
1047
+ * One child more or fewer under a parent (`null`: the top level): its pages' total, and the count its own item
1048
+ * carries in `hasChildrenKey` - a new copy of the item. A parent never opened reads its children from the new count,
1049
+ * so one that has none left loses its chevron; one whose children were loaded knows already.
1050
+ */
1051
+ countChild(parentKey, delta) {
1052
+ const parent = parentKey === null ? null : this.byKey.get(parentKey);
1053
+ if (parent === undefined)
1054
+ return;
1055
+ const pages = this.pagesOf(parent);
1056
+ if (pages?.total !== undefined)
1057
+ pages.total = Math.max(0, pages.total + delta);
1058
+ if (!parent || this.hasChildrenKey === undefined)
1059
+ return;
1060
+ const next = countedChildren(read(parent.item, this.hasChildrenKey), delta, pages?.total);
1061
+ if (next !== undefined)
1062
+ parent.item = { ...parent.item, [this.hasChildrenKey]: next };
1063
+ if (this.lazy && parent.children.length === 0 && parent.pages?.status === "idle")
1064
+ parent.pages = this.newPages(parent.item);
1065
+ }
390
1066
  createNode(item, key) {
391
1067
  return {
392
- uid: nextUid++,
1068
+ uid: this.nextUid++,
393
1069
  key,
394
1070
  value: read(item, this.valueKey),
395
1071
  item,
@@ -408,8 +1084,266 @@ export class TreeStore {
408
1084
  openLeaves: 0,
409
1085
  openChecked: 0,
410
1086
  draft: false,
1087
+ pages: this.lazy ? this.newPages(item) : undefined,
1088
+ partial: false,
411
1089
  };
412
1090
  }
1091
+ /** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
1092
+ newPages(item) {
1093
+ const known = this.hasChildrenKey === undefined ? undefined : treeHasChildren(read(item, this.hasChildrenKey));
1094
+ return known === false ? loadedPages() : idlePages();
1095
+ }
1096
+ /** Whether a parent (`null`: the top level) is still in the tree, so an answer for it applies. */
1097
+ owns(parent) {
1098
+ return parent === null || this.byKey.get(parent.key) === parent;
1099
+ }
1100
+ /** After a parent's pages changed state: its `partial` and counts along its path, and its rows. */
1101
+ changedPages(parent) {
1102
+ if (parent) {
1103
+ const path = this.path(parent).concat(parent);
1104
+ this.count(path, -1);
1105
+ for (let index = path.length - 1; index >= 0; index--)
1106
+ this.aggregate(path[index]);
1107
+ this.count(path, 1);
1108
+ }
1109
+ this.refreshBlock(parent);
1110
+ }
1111
+ /**
1112
+ * Creates nodes for items under a parent, after its children, and updates counts along the parent's path only. A
1113
+ * value already in the tree is passed over.
1114
+ */
1115
+ addNodes(parent, items) {
1116
+ const path = parent ? this.path(parent).concat(parent) : [];
1117
+ this.count(path, -1);
1118
+ const siblings = parent ? parent.children : this.roots;
1119
+ const added = [];
1120
+ for (const item of items) {
1121
+ const key = treeKey(read(item, this.valueKey));
1122
+ if (key === null || this.byKey.has(key))
1123
+ continue;
1124
+ const node = this.createNode(item, key);
1125
+ node.parent = parent;
1126
+ node.depth = parent ? parent.depth + 1 : 0;
1127
+ node.position = siblings.length;
1128
+ node.disabledPath = node.disabled || Boolean(parent?.disabledPath);
1129
+ node.checked = this.arrivalChecked(node);
1130
+ siblings.push(node);
1131
+ this.byKey.set(key, node);
1132
+ this.aggregate(node);
1133
+ added.push(node);
1134
+ }
1135
+ for (let index = path.length - 1; index >= 0; index--)
1136
+ this.aggregate(path[index]);
1137
+ this.count(path, 1);
1138
+ this.count(added, 1);
1139
+ return added;
1140
+ }
1141
+ /** Adds (1) or takes away (-1) nodes' share of the counts kept for a tree without `cascade`. */
1142
+ count(nodes, step) {
1143
+ for (const node of nodes) {
1144
+ if (node.checked)
1145
+ this.checkedNodes += step;
1146
+ if (node.locked)
1147
+ continue;
1148
+ this.openNodes += step;
1149
+ if (node.checked)
1150
+ this.openCheckedNodes += step;
1151
+ }
1152
+ }
1153
+ /** Whether an item that has just come starts checked: its value is waiting, a parent's value stands for it, or its own flag. */
1154
+ arrivalChecked(node) {
1155
+ if (this.pending.has(node.key))
1156
+ return true;
1157
+ if (this.cascade && this.valueMode === "checked" && this.pending.size > 0) {
1158
+ for (let parent = node.parent; parent; parent = parent.parent)
1159
+ if (this.pending.has(parent.key))
1160
+ return true;
1161
+ }
1162
+ return !this.valueGiven && this.isChecked(node.item);
1163
+ }
1164
+ /**
1165
+ * Whether a waiting value is settled now that its item is here, so the tree's own checks say it all:
1166
+ * - without `cascade`, at once - the item is its own value, and came checked;
1167
+ * - with `cascade`, a parent named in a value of leaves (or of every parent with a checked item) means nothing, as in
1168
+ * a tree loaded whole;
1169
+ * - with `cascade`, any other waits until everything under its item is loaded: an unloaded item, or a "checked"-mode
1170
+ * parent that stands for all of its items, loaded or not.
1171
+ */
1172
+ settle(node) {
1173
+ if (!this.pending.has(node.key))
1174
+ return;
1175
+ if (!this.cascade || (node.children.length > 0 && this.valueMode !== "checked") || !node.partial)
1176
+ this.pending.delete(node.key);
1177
+ }
1178
+ settleAll() {
1179
+ if (this.pending.size === 0)
1180
+ return;
1181
+ for (const key of [...this.pending.keys()]) {
1182
+ const node = this.byKey.get(key);
1183
+ if (node)
1184
+ this.settle(node);
1185
+ }
1186
+ }
1187
+ settleAround(parent, added) {
1188
+ if (this.pending.size === 0)
1189
+ return;
1190
+ for (const node of added)
1191
+ this.settle(node);
1192
+ for (let node = parent; node; node = node.parent)
1193
+ this.settle(node);
1194
+ }
1195
+ dropClaimsAbove(node) {
1196
+ if (this.pending.size === 0)
1197
+ return;
1198
+ for (let parent = node.parent; parent; parent = parent.parent)
1199
+ this.pending.delete(parent.key);
1200
+ }
1201
+ /** With `cascade`: the highest nodes whose items are all loaded - the roots, or below a partial node its loaded parts. */
1202
+ settledTops() {
1203
+ const list = [];
1204
+ const stack = [...this.roots].reverse();
1205
+ while (stack.length > 0) {
1206
+ const node = stack.pop();
1207
+ if (!node.partial)
1208
+ list.push(node);
1209
+ else
1210
+ for (let index = node.children.length - 1; index >= 0; index--)
1211
+ stack.push(node.children[index]);
1212
+ }
1213
+ return list;
1214
+ }
1215
+ /**
1216
+ * Fresh items in place of a parent's children: a node whose value is still there is kept (with what is expanded and
1217
+ * loaded under it) and takes the fresh item; one not there goes, unless it is a draft being named.
1218
+ */
1219
+ reconcile(parent, items) {
1220
+ const siblings = parent ? parent.children : this.roots;
1221
+ const old = new Map(siblings.map(node => [node.key, node]));
1222
+ const next = [];
1223
+ for (const item of items) {
1224
+ const key = treeKey(read(item, this.valueKey));
1225
+ if (key === null)
1226
+ continue;
1227
+ const kept = old.get(key);
1228
+ if (kept) {
1229
+ old.delete(key);
1230
+ kept.item = item;
1231
+ kept.value = read(item, this.valueKey);
1232
+ kept.label = labelOf(item, this.labelKey);
1233
+ kept.disabled = this.isDisabled(item);
1234
+ // Nothing loaded under it: what the fresh item says about its children counts.
1235
+ if (kept.children.length === 0 && kept.pages && kept.pages.status !== "loading")
1236
+ kept.pages = this.newPages(item);
1237
+ next.push(kept);
1238
+ }
1239
+ else if (!this.byKey.has(key)) {
1240
+ const node = this.createNode(item, key);
1241
+ node.parent = parent;
1242
+ node.depth = parent ? parent.depth + 1 : 0;
1243
+ node.checked = this.arrivalChecked(node);
1244
+ this.byKey.set(key, node);
1245
+ next.push(node);
1246
+ }
1247
+ }
1248
+ for (const gone of old.values()) {
1249
+ if (gone.draft) {
1250
+ next.push(gone);
1251
+ continue;
1252
+ }
1253
+ for (const each of preOrder([gone])) {
1254
+ this.byKey.delete(each.key);
1255
+ this.statusRows.delete(each.key);
1256
+ }
1257
+ gone.parent = null;
1258
+ }
1259
+ siblings.splice(0, siblings.length, ...next);
1260
+ }
1261
+ /** 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. */
1262
+ refreshBlock(parent) {
1263
+ const before = this.rows;
1264
+ let start = 0;
1265
+ let end = before.length;
1266
+ if (parent) {
1267
+ if (!parent.expanded)
1268
+ return;
1269
+ const index = this.isVisible(parent) ? this.indexOf(parent) : -1;
1270
+ if (index < 0)
1271
+ return;
1272
+ start = index + 1;
1273
+ end = start;
1274
+ while (end < before.length && before[end].depth > parent.depth)
1275
+ end++;
1276
+ }
1277
+ this.rows = before.slice(0, start).concat(this.visibleRows(parent ? parent.children : this.roots, parent), before.slice(end));
1278
+ }
1279
+ /** 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. */
1280
+ visibleRows(nodes, parent) {
1281
+ const list = [];
1282
+ const stack = [];
1283
+ const own = this.statusRowOf(parent);
1284
+ if (own)
1285
+ stack.push(own);
1286
+ for (let index = nodes.length - 1; index >= 0; index--)
1287
+ stack.push(nodes[index]);
1288
+ while (stack.length > 0) {
1289
+ const node = stack.pop();
1290
+ list.push(node);
1291
+ if (!node.expanded)
1292
+ continue;
1293
+ if (this.lazy) {
1294
+ const status = this.statusRowOf(node);
1295
+ if (status)
1296
+ stack.push(status);
1297
+ }
1298
+ for (let index = node.children.length - 1; index >= 0; index--)
1299
+ stack.push(node.children[index]);
1300
+ }
1301
+ return list;
1302
+ }
1303
+ /** The status row an expanded parent (`null`: the top level) shows after its children, if its pages need one. */
1304
+ statusRowOf(parent) {
1305
+ if (!this.lazy)
1306
+ return undefined;
1307
+ const pages = this.pagesOf(parent);
1308
+ if (!pages)
1309
+ return undefined;
1310
+ // An open parent not asked yet is about to be: it shows as loading.
1311
+ const kind = pages.status === "loading" || pages.status === "idle" ? "loading" : pages.status === "failed" ? "error" : pages.hasMore ? "more" : null;
1312
+ if (kind === null)
1313
+ return undefined;
1314
+ const parentKey = parent ? parent.key : "";
1315
+ let row = this.statusRows.get(parentKey);
1316
+ if (!row || row.parent !== parent) {
1317
+ // Not built by createNode: the item callbacks (isItemDisabled) are never handed an empty object.
1318
+ row = {
1319
+ uid: this.nextUid++,
1320
+ key: STATUS_KEY + parentKey,
1321
+ value: "",
1322
+ item: EMPTY_ITEM,
1323
+ label: "",
1324
+ parent,
1325
+ children: [],
1326
+ depth: 0,
1327
+ position: 0,
1328
+ disabled: false,
1329
+ disabledPath: false,
1330
+ locked: false,
1331
+ expanded: false,
1332
+ checked: false,
1333
+ leaves: 0,
1334
+ checkedLeaves: 0,
1335
+ openLeaves: 0,
1336
+ openChecked: 0,
1337
+ draft: false,
1338
+ partial: false,
1339
+ };
1340
+ this.statusRows.set(parentKey, row);
1341
+ }
1342
+ row.status = kind;
1343
+ row.depth = parent ? parent.depth + 1 : 0;
1344
+ row.disabledPath = Boolean(parent?.disabledPath);
1345
+ return row;
1346
+ }
413
1347
  keysOf(values) {
414
1348
  const keys = new Set();
415
1349
  for (const value of values) {
@@ -434,51 +1368,53 @@ export class TreeStore {
434
1368
  for (const node of ordered) {
435
1369
  node.children.forEach((child, index) => (child.position = index));
436
1370
  node.disabledPath = node.disabled || Boolean(node.parent?.disabledPath);
1371
+ }
1372
+ for (let index = ordered.length - 1; index >= 0; index--)
1373
+ this.aggregate(ordered[index]);
1374
+ this.checkedNodes = 0;
1375
+ this.openNodes = 0;
1376
+ this.openCheckedNodes = 0;
1377
+ this.count(ordered, 1);
1378
+ }
1379
+ /**
1380
+ * A node's lock, `partial`, and counts, from its own flags and its children's (already worked out): what `recount`
1381
+ * does for every node, and what a page of children does for each node on its parent's path.
1382
+ */
1383
+ aggregate(node) {
1384
+ const pages = node.pages;
1385
+ const own = pages !== undefined && (pages.status !== "ready" || pages.hasMore);
1386
+ if (node.children.length === 0) {
1387
+ node.partial = own;
437
1388
  node.locked = node.disabledPath;
1389
+ // A lazy node not loaded yet is a leaf whose box cannot be changed with cascade: it shows what the value says.
1390
+ const fixed = node.locked || (this.cascade && own);
1391
+ node.leaves = 1;
1392
+ node.checkedLeaves = node.checked ? 1 : 0;
1393
+ node.openLeaves = fixed ? 0 : 1;
1394
+ node.openChecked = fixed ? 0 : node.checkedLeaves;
1395
+ return;
438
1396
  }
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
- }
1397
+ let leaves = 0;
1398
+ let checked = 0;
1399
+ let openLeaves = 0;
1400
+ let openChecked = 0;
1401
+ let allLocked = true;
1402
+ let partial = own;
1403
+ for (const child of node.children) {
1404
+ leaves += child.leaves;
1405
+ checked += child.checkedLeaves;
1406
+ openLeaves += child.openLeaves;
1407
+ openChecked += child.openChecked;
1408
+ allLocked &&= child.locked;
1409
+ partial ||= child.partial;
478
1410
  }
479
- this.checkedNodes = checkedNodes;
480
- this.openNodes = openNodes;
481
- this.openCheckedNodes = openCheckedNodes;
1411
+ node.leaves = leaves;
1412
+ node.checkedLeaves = checked;
1413
+ node.openLeaves = openLeaves;
1414
+ node.openChecked = openChecked;
1415
+ node.partial = partial;
1416
+ // A parent's box follows its items only with cascade; without it, a parent stays free.
1417
+ node.locked = node.disabledPath || (this.cascade && allLocked);
482
1418
  }
483
1419
  /** Sets every open leaf under a node (the node itself for a leaf), updating counts up the path of each leaf that changes. */
484
1420
  setLeaves(node, checked) {
@@ -537,8 +1473,15 @@ export class TreeStore {
537
1473
  // A subtree without a checked leaf adds nothing.
538
1474
  if (node.checkedLeaves === 0)
539
1475
  continue;
540
- if (node.children.length === 0 || mode === "any" || (mode === "checked" && node.checkedLeaves === node.leaves))
1476
+ if (node.partial) {
1477
+ // Not everything under it is loaded: it is in the value only when the value names it (see `value`), except
1478
+ // as a parent with a checked item in "any" mode, which it is.
1479
+ if (mode === "any" && node.children.length > 0)
1480
+ list.push(node);
1481
+ }
1482
+ else if (node.children.length === 0 || mode === "any" || (mode === "checked" && node.checkedLeaves === node.leaves)) {
541
1483
  list.push(node);
1484
+ }
542
1485
  }
543
1486
  for (let index = node.children.length - 1; index >= 0; index--)
544
1487
  stack.push(node.children[index]);
@@ -546,6 +1489,34 @@ export class TreeStore {
546
1489
  return list;
547
1490
  }
548
1491
  }
1492
+ /** Placeholder item of a status row. */
1493
+ const EMPTY_ITEM = Object.freeze({});
1494
+ function idlePages() {
1495
+ return { status: "idle", loaded: 0, hasMore: true, stale: false, request: 0 };
1496
+ }
1497
+ /** Pages of a node whose children are all known: given in the items, created here, or none at all. */
1498
+ function loadedPages() {
1499
+ return { status: "ready", loaded: 0, hasMore: false, stale: false, request: 0 };
1500
+ }
1501
+ /** Whether another page follows the one that came with `count` items. A page with nothing in it ends the list, whatever it says. */
1502
+ function morePages(pages, page, count, take) {
1503
+ if (count === 0)
1504
+ return false;
1505
+ if (typeof page.hasMore === "boolean")
1506
+ return page.hasMore;
1507
+ if (pages.total !== undefined)
1508
+ return pages.loaded < pages.total;
1509
+ return count >= take;
1510
+ }
1511
+ function valueMap(values) {
1512
+ const map = new Map();
1513
+ for (const value of values) {
1514
+ const key = treeKey(value);
1515
+ if (key !== null && !map.has(key))
1516
+ map.set(key, value);
1517
+ }
1518
+ return map;
1519
+ }
549
1520
  /** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
550
1521
  export function preOrder(nodes) {
551
1522
  const list = [];
@@ -558,19 +1529,6 @@ export function preOrder(nodes) {
558
1529
  }
559
1530
  return list;
560
1531
  }
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
1532
  function labelOf(item, key) {
575
1533
  const label = read(item, key);
576
1534
  return label === null || label === undefined ? "" : String(label);