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.
- package/CHANGELOG.md +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +56 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +125 -16
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- 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.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- 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/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- 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.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- 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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
223
|
-
changed = this.setChecked(
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
282
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
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);
|