nexus-shared 3.0.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.
- package/CHANGELOG.md +442 -0
- package/README.md +1123 -1000
- package/dist/Client.Index.d.ts +6 -0
- package/dist/Client.Index.js +6 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +486 -206
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +38 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +9 -14
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.js +4 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/InputRenderer.js +3 -0
- package/dist/Components/Inputs/NumberBox.js +26 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TextArea.js +4 -1
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +55 -5
- package/dist/Components/Inputs/TreeView.js +372 -23
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
- package/dist/Components/Layouts/ThemePicker.js +89 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +252 -62
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
- package/dist/Components/Viewers/DataTableParts.js +42 -13
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +101 -8
- package/dist/Helpers/CrudBackend.js +172 -11
- package/dist/Helpers/CrudColumns.d.ts +51 -0
- package/dist/Helpers/CrudColumns.js +69 -0
- package/dist/Helpers/CrudHelpers.d.ts +50 -5
- package/dist/Helpers/CrudHelpers.js +109 -12
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/InputParamsHelpers.js +8 -0
- package/dist/Helpers/MessageBuilder.js +3 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RequestCache.d.ts +35 -0
- package/dist/Helpers/RequestCache.js +113 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +7 -4
- package/dist/Helpers/TableHelpers.js +15 -6
- package/dist/Helpers/TableJoins.d.ts +70 -0
- package/dist/Helpers/TableJoins.js +250 -0
- package/dist/Helpers/TableStore.d.ts +16 -3
- package/dist/Helpers/TableStore.js +25 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/TreeStore.d.ts +225 -11
- package/dist/Helpers/TreeStore.js +683 -90
- package/dist/Helpers/UserPreferences.d.ts +22 -0
- package/dist/Helpers/UserPreferences.js +116 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +37 -6
- package/dist/Interfaces/InputInterfaces.d.ts +24 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
- package/dist/Interfaces/PreferenceInterfaces.js +25 -0
- package/dist/Interfaces/TableInterfaces.d.ts +145 -16
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/JoinSources.d.ts +48 -0
- package/dist/Services/JoinSources.js +74 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +6 -0
- package/dist/Shared.Index.js +6 -0
- package/package.json +4 -2
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +51 -4
- package/src/Styles/Nexus.Button.css +27 -0
- package/src/Styles/Nexus.Calendar.css +4 -6
- package/src/Styles/Nexus.Chat.css +5 -51
- package/src/Styles/Nexus.Choice.css +4 -6
- package/src/Styles/Nexus.Crud.css +16 -9
- package/src/Styles/Nexus.Dropdown.css +12 -61
- package/src/Styles/Nexus.Editor.css +4 -6
- package/src/Styles/Nexus.Excel.css +34 -15
- package/src/Styles/Nexus.Form.css +25 -67
- package/src/Styles/Nexus.Index.css +1 -0
- package/src/Styles/Nexus.Input.css +794 -637
- package/src/Styles/Nexus.Markdown.css +0 -28
- package/src/Styles/Nexus.Menu.css +8 -30
- package/src/Styles/Nexus.Popup.css +22 -29
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +51 -10
- package/src/Styles/Nexus.Slider.css +3 -5
- package/src/Styles/Nexus.Tab.Buttons.css +20 -7
- package/src/Styles/Nexus.Table.css +1387 -1296
- package/src/Styles/Nexus.Theme.Picker.css +342 -232
- package/src/Styles/Nexus.Time.css +9 -12
- package/src/Styles/Nexus.Toast.css +6 -8
- package/src/Styles/Nexus.Tokens.css +17 -0
- package/src/Styles/Nexus.Tree.css +76 -11
|
@@ -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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
223
|
-
changed = this.setChecked(
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
282
|
-
|
|
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
|
|
440
|
-
let
|
|
441
|
-
let
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
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);
|