nexus-shared 3.2.0 → 4.1.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 +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- 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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -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 +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -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 +182 -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/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- 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 +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -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 +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- 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/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -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.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -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 +158 -0
- package/dist/Helpers/FileAdapter.js +173 -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 +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -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 +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- 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 +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- 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 +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- 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.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
* itself: the view asks, and tells the store what came back (`beginLoad`, `receivePage`, `failLoad`).
|
|
13
13
|
*/
|
|
14
14
|
import { RequestCache } from "./RequestCache.js";
|
|
15
|
+
/** Where a tree keeps the filter chosen in this browser: per `key` and `viewer`, nothing without a viewer. */
|
|
16
|
+
export function treeFilterStorageKey(remember) {
|
|
17
|
+
const viewer = remember?.viewer?.trim();
|
|
18
|
+
return remember?.key && viewer ? `nx-tree-filter:${remember.key}:${viewer}` : null;
|
|
19
|
+
}
|
|
20
|
+
/** The filter on: the one chosen, else the default, else the first; `undefined` without filters. An id no filter has is passed over. */
|
|
21
|
+
export function shownTreeFilter(filters, chosen, defaultId) {
|
|
22
|
+
if (!filters || filters.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return filters.find(filter => filter.id === chosen) ?? filters.find(filter => filter.id === defaultId) ?? filters[0];
|
|
25
|
+
}
|
|
15
26
|
/** Children's pages kept for every lazy tree on the page that names a `cacheKey`, for 5 minutes: see `RequestCache`. */
|
|
16
27
|
export const treePages = new RequestCache();
|
|
17
28
|
/** The start of every key a tree keeps its pages under, or one parent's pages (`null`: the top level). */
|
|
@@ -36,6 +47,212 @@ export function expireTreePages(cacheKey, parent) {
|
|
|
36
47
|
const parents = new Set(parent.map(value => treeKey(value) ?? ""));
|
|
37
48
|
return treePages.drop(key => key.startsWith(base) && parents.has(key.slice(base.length, key.indexOf("\u0000", base.length))));
|
|
38
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Writes changes made elsewhere into the pages kept for a tree's source, and into every tree on screen that keeps its
|
|
52
|
+
* pages under `cacheKey` - so a record saved in a form, moved, or sent to the trash shows in the tree at once, and
|
|
53
|
+
* leaving the page and coming back reads nothing again for it. Items go where the parent's children are known (see
|
|
54
|
+
* `TreeStore.placeItem`); a parent whose children were never loaded only counts one more or one fewer, and its first page
|
|
55
|
+
* comes from the server when it is opened, as it would have anyway.
|
|
56
|
+
*/
|
|
57
|
+
export function writeTreeChanges(cacheKey, changes, rules = {}) {
|
|
58
|
+
if (changes.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
patchTreePages(treePages, cacheKey, changes, rules);
|
|
61
|
+
for (const listener of [...(treeListeners.get(cacheKey) ?? [])])
|
|
62
|
+
listener(changes, rules);
|
|
63
|
+
}
|
|
64
|
+
const treeListeners = new Map();
|
|
65
|
+
/** Hears the changes written for `cacheKey`: what a tree on screen does to write them into itself. Returns the way out. */
|
|
66
|
+
export function subscribeTreeChanges(cacheKey, listener) {
|
|
67
|
+
let set = treeListeners.get(cacheKey);
|
|
68
|
+
if (!set)
|
|
69
|
+
treeListeners.set(cacheKey, (set = new Set()));
|
|
70
|
+
set.add(listener);
|
|
71
|
+
return () => {
|
|
72
|
+
set.delete(listener);
|
|
73
|
+
if (set.size === 0 && treeListeners.get(cacheKey) === set)
|
|
74
|
+
treeListeners.delete(cacheKey);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The kept pages under `cacheKey` with the changes written in (what `writeTreeChanges` does to `treePages`). A parent's
|
|
79
|
+
* pages are read as one list from its first child, the changes applied to it, and the list written back in pages of the
|
|
80
|
+
* same size, each with the parent's total - so a tree reading them again, page by page, asks the server for exactly the
|
|
81
|
+
* children it does not have. A page still on its way for a changed parent is dropped: its answer may predate the change.
|
|
82
|
+
*/
|
|
83
|
+
export function patchTreePages(cache, cacheKey, changes, rules = {}) {
|
|
84
|
+
const valueKey = rules.valueKey ?? TREE_DEFAULT_KEYS.value;
|
|
85
|
+
const keyOf = (item) => treeKey(read(item, valueKey));
|
|
86
|
+
const groups = keptChildren(cache, cacheKey);
|
|
87
|
+
const of = (parent) => groups.filter(group => group.parent === parent);
|
|
88
|
+
/** A parent's count, on its own item, wherever that item is kept. */
|
|
89
|
+
const count = (parent, delta) => {
|
|
90
|
+
if (parent === "" || rules.hasChildrenKey === undefined)
|
|
91
|
+
return;
|
|
92
|
+
const field = rules.hasChildrenKey;
|
|
93
|
+
const known = of(parent).find(group => group.total !== undefined)?.total;
|
|
94
|
+
for (const group of groups) {
|
|
95
|
+
const index = group.items.findIndex(item => keyOf(item) === parent);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
continue;
|
|
98
|
+
const item = group.items[index];
|
|
99
|
+
const next = countedChildren(read(item, field), delta, known);
|
|
100
|
+
if (next === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
group.items[index] = { ...item, [field]: next };
|
|
103
|
+
group.changed = true;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (const change of changes) {
|
|
107
|
+
if (change.type === "put") {
|
|
108
|
+
const key = keyOf(change.item);
|
|
109
|
+
if (key === null)
|
|
110
|
+
continue;
|
|
111
|
+
const parent = treeKey(change.parent) ?? "";
|
|
112
|
+
let from = change.before === false ? false : change.before === undefined ? undefined : (treeKey(change.before) ?? "");
|
|
113
|
+
// Out of the list that holds it, remembering where, for an item that stays where it was.
|
|
114
|
+
const held = new Map();
|
|
115
|
+
for (const group of groups) {
|
|
116
|
+
const index = group.items.findIndex(item => keyOf(item) === key);
|
|
117
|
+
if (index < 0)
|
|
118
|
+
continue;
|
|
119
|
+
from ??= group.parent;
|
|
120
|
+
group.items.splice(index, 1);
|
|
121
|
+
group.changed = true;
|
|
122
|
+
held.set(group, index);
|
|
123
|
+
}
|
|
124
|
+
const moved = typeof from === "string" && from !== parent;
|
|
125
|
+
if (moved) {
|
|
126
|
+
for (const group of of(from)) {
|
|
127
|
+
if (group.total !== undefined)
|
|
128
|
+
group.total = Math.max(0, group.total - 1);
|
|
129
|
+
group.changed = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const group of of(parent)) {
|
|
133
|
+
if (!group.listed)
|
|
134
|
+
continue;
|
|
135
|
+
const at = rules.compare ? insertionIndex(group.items, change.item, rules.compare) : Math.min(held.get(group) ?? group.items.length, group.items.length);
|
|
136
|
+
// After the children kept while more are to come: it is in a page not read yet.
|
|
137
|
+
if (at < group.items.length || !group.hasMore)
|
|
138
|
+
group.items.splice(at, 0, change.item);
|
|
139
|
+
if ((moved || from === false) && group.total !== undefined)
|
|
140
|
+
group.total++;
|
|
141
|
+
group.changed = true;
|
|
142
|
+
}
|
|
143
|
+
if (moved)
|
|
144
|
+
count(from, -1);
|
|
145
|
+
if (moved || from === false)
|
|
146
|
+
count(parent, 1);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const key = treeKey(change.value);
|
|
150
|
+
if (key === null)
|
|
151
|
+
continue;
|
|
152
|
+
let from = change.parent === undefined ? undefined : (treeKey(change.parent) ?? "");
|
|
153
|
+
let found = false;
|
|
154
|
+
for (const group of groups) {
|
|
155
|
+
const index = group.items.findIndex(item => keyOf(item) === key);
|
|
156
|
+
if (index < 0)
|
|
157
|
+
continue;
|
|
158
|
+
from ??= group.parent;
|
|
159
|
+
found = true;
|
|
160
|
+
group.items.splice(index, 1);
|
|
161
|
+
if (group.total !== undefined)
|
|
162
|
+
group.total = Math.max(0, group.total - 1);
|
|
163
|
+
group.changed = true;
|
|
164
|
+
}
|
|
165
|
+
if (from === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
// Not among the children kept: one of those not read yet, which the total still counted.
|
|
168
|
+
if (!found) {
|
|
169
|
+
for (const group of of(from)) {
|
|
170
|
+
if (group.total !== undefined)
|
|
171
|
+
group.total = Math.max(0, group.total - 1);
|
|
172
|
+
group.changed = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
count(from, -1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const group of groups) {
|
|
179
|
+
if (!group.changed)
|
|
180
|
+
continue;
|
|
181
|
+
const prefix = treePagesKey(cacheKey, group.parent === "" ? null : group.parent);
|
|
182
|
+
const kept = new Set(group.keys);
|
|
183
|
+
cache.drop(key => kept.has(key) || (key.startsWith(prefix) && cache.isLoading(key)));
|
|
184
|
+
// A list emptied while more are to come has nothing to start from: the next open asks the server.
|
|
185
|
+
if (!group.listed || (group.items.length === 0 && group.hasMore))
|
|
186
|
+
continue;
|
|
187
|
+
const hasMore = group.total !== undefined ? group.items.length < group.total : group.hasMore;
|
|
188
|
+
const pages = Math.max(1, Math.ceil(group.items.length / group.take));
|
|
189
|
+
for (let page = 0; page < pages; page++) {
|
|
190
|
+
const items = group.items.slice(page * group.take, (page + 1) * group.take);
|
|
191
|
+
const last = page === pages - 1;
|
|
192
|
+
cache.set(treePageKey(cacheKey, group.parent === "" ? null : group.parent, page * group.take, group.take), { items, total: group.total, hasMore: last ? hasMore : true }, group.at);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** The kept pages under `cacheKey`, by parent and page size, each read as one list from the parent's first child. */
|
|
197
|
+
function keptChildren(cache, cacheKey) {
|
|
198
|
+
const base = treePagesKey(cacheKey);
|
|
199
|
+
const found = new Map();
|
|
200
|
+
for (const { key, value, at } of cache.entries(base)) {
|
|
201
|
+
const [parent, skipText, takeText] = key.slice(base.length).split(NUL);
|
|
202
|
+
const skip = Number(skipText);
|
|
203
|
+
const take = Number(takeText);
|
|
204
|
+
if (parent === undefined || !Number.isInteger(skip) || !Number.isInteger(take) || take < 1)
|
|
205
|
+
continue;
|
|
206
|
+
const id = `${parent}${NUL}${take}`;
|
|
207
|
+
let group = found.get(id);
|
|
208
|
+
if (!group)
|
|
209
|
+
found.set(id, (group = { parent, take, pages: [] }));
|
|
210
|
+
group.pages.push({ key, skip, page: value, at });
|
|
211
|
+
}
|
|
212
|
+
const groups = [];
|
|
213
|
+
for (const { parent, take, pages } of found.values()) {
|
|
214
|
+
pages.sort((a, b) => a.skip - b.skip);
|
|
215
|
+
const items = [];
|
|
216
|
+
let total;
|
|
217
|
+
let hasMore = false;
|
|
218
|
+
let at = Infinity;
|
|
219
|
+
for (const { skip, page, at: loadedAt } of pages) {
|
|
220
|
+
// A page that does not follow the last one cannot be placed: the list ends before it.
|
|
221
|
+
if (skip !== items.length)
|
|
222
|
+
break;
|
|
223
|
+
const pageItems = Array.isArray(page?.items) ? page.items : [];
|
|
224
|
+
items.push(...pageItems);
|
|
225
|
+
if (typeof page.total === "number" && Number.isFinite(page.total))
|
|
226
|
+
total = page.total;
|
|
227
|
+
hasMore = pageItems.length === 0 ? false : typeof page.hasMore === "boolean" ? page.hasMore : total !== undefined ? items.length < total : pageItems.length >= take;
|
|
228
|
+
at = Math.min(at, loadedAt);
|
|
229
|
+
if (!hasMore)
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const listed = pages[0]?.skip === 0;
|
|
233
|
+
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 });
|
|
234
|
+
}
|
|
235
|
+
return groups;
|
|
236
|
+
}
|
|
237
|
+
/** Where an item goes among siblings in `compare`'s order: before the first that sorts after it. */
|
|
238
|
+
function insertionIndex(items, item, compare) {
|
|
239
|
+
const index = items.findIndex(other => compare(item, other) < 0);
|
|
240
|
+
return index < 0 ? items.length : index;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A parent's has-children field after it gained (1) or lost (-1) a child: a count moves by one, `true` stays true and
|
|
244
|
+
* becomes it, and `false` comes only when the parent's total says none are left. `undefined` when nothing changes.
|
|
245
|
+
*/
|
|
246
|
+
function countedChildren(value, delta, total) {
|
|
247
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
248
|
+
return Math.max(0, value + delta);
|
|
249
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)))
|
|
250
|
+
return Math.max(0, Number(value) + delta);
|
|
251
|
+
if (delta > 0)
|
|
252
|
+
return value === true ? undefined : true;
|
|
253
|
+
return value === true && total === 0 ? false : undefined;
|
|
254
|
+
}
|
|
255
|
+
const NUL = "\u0000";
|
|
39
256
|
/** A has-children field read as the tree reads it: a boolean, or a count above 0; `undefined` when it says nothing. */
|
|
40
257
|
export function treeHasChildren(value) {
|
|
41
258
|
if (typeof value === "boolean")
|
|
@@ -104,6 +321,11 @@ export class TreeStore {
|
|
|
104
321
|
openNodes = 0;
|
|
105
322
|
openCheckedNodes = 0;
|
|
106
323
|
indexCache = null;
|
|
324
|
+
/** The leaves shown (`setFilter`); `null`: all. */
|
|
325
|
+
match = null;
|
|
326
|
+
/** Goes up whenever a node's counts are worked out again, so what is kept from them knows it is old. */
|
|
327
|
+
counted = 0;
|
|
328
|
+
shownRoots = null;
|
|
107
329
|
constructor(items = [], options = {}, value) {
|
|
108
330
|
this.valueKey = options.valueKey ?? TREE_DEFAULT_KEYS.value;
|
|
109
331
|
this.parentKey = options.parentKey ?? TREE_DEFAULT_KEYS.parent;
|
|
@@ -116,8 +338,40 @@ export class TreeStore {
|
|
|
116
338
|
this.isChecked = options.isItemChecked ?? defaultChecked;
|
|
117
339
|
if (this.lazy && options.lazyRoots)
|
|
118
340
|
this.rootPages = idlePages();
|
|
341
|
+
this.match = options.filter ?? null;
|
|
119
342
|
this.setItems(items, value);
|
|
120
343
|
}
|
|
344
|
+
/** Whether a filter is on (`setFilter`). */
|
|
345
|
+
get filtered() {
|
|
346
|
+
return this.match !== null;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Shows only the leaves `match` takes, and the parents that hold one (`null`: every item). Nothing is checked or
|
|
350
|
+
* unchecked by it, and the value is the same whatever filter is on. While one is on, a parent's box, its count, and a
|
|
351
|
+
* check of it - or of "select all" - work on the leaves shown only: checking a parent checks its shown leaves, and a
|
|
352
|
+
* leaf the filter hides keeps its check exactly as it was. Expanded items stay expanded.
|
|
353
|
+
*/
|
|
354
|
+
setFilter(match) {
|
|
355
|
+
this.match = match;
|
|
356
|
+
this.recount(preOrder(this.roots));
|
|
357
|
+
this.rows = this.visibleRows(this.roots, null);
|
|
358
|
+
}
|
|
359
|
+
/** Children of a parent (`null`: the top level) that are rows of the filtered tree: all of them without a filter. */
|
|
360
|
+
shownChildCount(parent) {
|
|
361
|
+
if (parent)
|
|
362
|
+
return parent.shownChildren;
|
|
363
|
+
if (this.match === null)
|
|
364
|
+
return this.roots.length;
|
|
365
|
+
// Counted once per change of counts, not once per row drawn.
|
|
366
|
+
if (this.shownRoots?.counted !== this.counted) {
|
|
367
|
+
let count = 0;
|
|
368
|
+
for (const root of this.roots)
|
|
369
|
+
if (root.shown)
|
|
370
|
+
count++;
|
|
371
|
+
this.shownRoots = { counted: this.counted, count };
|
|
372
|
+
}
|
|
373
|
+
return this.shownRoots.count;
|
|
374
|
+
}
|
|
121
375
|
/** Items in the tree, drafts included. */
|
|
122
376
|
get size() {
|
|
123
377
|
return this.byKey.size;
|
|
@@ -250,15 +504,22 @@ export class TreeStore {
|
|
|
250
504
|
* unloaded items are not in the value - unless the value names it (`valueMode: "checked"`, where it stands for all).
|
|
251
505
|
*/
|
|
252
506
|
state(node) {
|
|
507
|
+
return this.stateOver(node, node.shownChecked, node.shownLeaves);
|
|
508
|
+
}
|
|
509
|
+
/** The state of a parent over all of its leaves, whatever the filter shows. */
|
|
510
|
+
wholeState(node) {
|
|
511
|
+
return this.stateOver(node, node.checkedLeaves, node.leaves);
|
|
512
|
+
}
|
|
513
|
+
stateOver(node, checked, leaves) {
|
|
253
514
|
if (!this.cascade || node.children.length === 0)
|
|
254
515
|
return node.checked ? "checked" : "unchecked";
|
|
255
516
|
if (node.partial)
|
|
256
|
-
return this.pending.has(node.key) ? "checked" :
|
|
257
|
-
if (
|
|
517
|
+
return this.pending.has(node.key) ? "checked" : checked > 0 ? "mixed" : "unchecked";
|
|
518
|
+
if (checked === 0)
|
|
258
519
|
return "unchecked";
|
|
259
|
-
return
|
|
520
|
+
return checked === leaves ? "checked" : "mixed";
|
|
260
521
|
}
|
|
261
|
-
/** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. */
|
|
522
|
+
/** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. The filter does not narrow it. */
|
|
262
523
|
counts() {
|
|
263
524
|
if (!this.cascade)
|
|
264
525
|
return { checked: this.checkedNodes, total: this.byKey.size };
|
|
@@ -270,6 +531,29 @@ export class TreeStore {
|
|
|
270
531
|
}
|
|
271
532
|
return { checked, total };
|
|
272
533
|
}
|
|
534
|
+
/** `counts` over what the filter shows: the leaves shown with `cascade`, the rows of items without it. */
|
|
535
|
+
shownCounts() {
|
|
536
|
+
if (this.match === null)
|
|
537
|
+
return this.counts();
|
|
538
|
+
let checked = 0;
|
|
539
|
+
let total = 0;
|
|
540
|
+
if (this.cascade) {
|
|
541
|
+
for (const root of this.roots) {
|
|
542
|
+
checked += root.shownChecked;
|
|
543
|
+
total += root.shownLeaves;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
for (const node of this.byKey.values()) {
|
|
548
|
+
if (!node.shown)
|
|
549
|
+
continue;
|
|
550
|
+
total++;
|
|
551
|
+
if (node.checked)
|
|
552
|
+
checked++;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return { checked, total };
|
|
556
|
+
}
|
|
273
557
|
/**
|
|
274
558
|
* The checked values, in tree order, following `valueMode`. In a lazy tree, values whose items are not loaded yet
|
|
275
559
|
* (or not settled) follow the others, so a saved value survives until its items come.
|
|
@@ -337,9 +621,12 @@ export class TreeStore {
|
|
|
337
621
|
return false;
|
|
338
622
|
if (!this.cascade)
|
|
339
623
|
return this.setNodeChecked(node, !node.checked);
|
|
340
|
-
return this.setChecked(node, node.
|
|
624
|
+
return this.setChecked(node, node.shownOpenChecked < node.shownOpenLeaves);
|
|
341
625
|
}
|
|
342
|
-
/**
|
|
626
|
+
/**
|
|
627
|
+
* Checks or unchecks a node; with `cascade`, every item under it that is not locked - and, while a filter is on, that
|
|
628
|
+
* the filter shows: the hidden ones keep their checks. Returns whether anything changed.
|
|
629
|
+
*/
|
|
343
630
|
setChecked(node, checked) {
|
|
344
631
|
if (node.locked || node.status)
|
|
345
632
|
return false;
|
|
@@ -353,7 +640,7 @@ export class TreeStore {
|
|
|
353
640
|
this.dropClaimsAbove(node);
|
|
354
641
|
return changed;
|
|
355
642
|
}
|
|
356
|
-
/** The state of a "select all" box, over the items that are not locked; `null` when every
|
|
643
|
+
/** The state of a "select all" box, over the items that are not locked (and shown); `null` when every one is locked. */
|
|
357
644
|
allState() {
|
|
358
645
|
let open = this.openNodes;
|
|
359
646
|
let checked = this.openCheckedNodes;
|
|
@@ -361,8 +648,19 @@ export class TreeStore {
|
|
|
361
648
|
open = 0;
|
|
362
649
|
checked = 0;
|
|
363
650
|
for (const root of this.roots) {
|
|
364
|
-
open += root.
|
|
365
|
-
checked += root.
|
|
651
|
+
open += root.shownOpenLeaves;
|
|
652
|
+
checked += root.shownOpenChecked;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
else if (this.match !== null) {
|
|
656
|
+
open = 0;
|
|
657
|
+
checked = 0;
|
|
658
|
+
for (const node of this.byKey.values()) {
|
|
659
|
+
if (!node.shown || node.locked)
|
|
660
|
+
continue;
|
|
661
|
+
open++;
|
|
662
|
+
if (node.checked)
|
|
663
|
+
checked++;
|
|
366
664
|
}
|
|
367
665
|
}
|
|
368
666
|
if (open === 0)
|
|
@@ -375,9 +673,9 @@ export class TreeStore {
|
|
|
375
673
|
return state !== null && this.checkAll(state !== "checked");
|
|
376
674
|
}
|
|
377
675
|
/**
|
|
378
|
-
* Checks or unchecks every item that is not locked. Returns whether anything changed. In a
|
|
379
|
-
* a node whose items are not all loaded is passed over and the loaded parts under it are
|
|
380
|
-
* items are not loaded stay as they are.
|
|
676
|
+
* Checks or unchecks every item that is not locked, and that the filter shows. Returns whether anything changed. In a
|
|
677
|
+
* lazy tree with `cascade`, a node whose items are not all loaded is passed over and the loaded parts under it are
|
|
678
|
+
* changed; values whose items are not loaded stay as they are.
|
|
381
679
|
*/
|
|
382
680
|
checkAll(checked) {
|
|
383
681
|
let changed = false;
|
|
@@ -387,7 +685,7 @@ export class TreeStore {
|
|
|
387
685
|
}
|
|
388
686
|
else {
|
|
389
687
|
for (const node of preOrder(this.roots))
|
|
390
|
-
changed = (!node.locked && this.setNodeChecked(node, checked)) || changed;
|
|
688
|
+
changed = (!node.locked && node.shown && this.setNodeChecked(node, checked)) || changed;
|
|
391
689
|
}
|
|
392
690
|
return changed;
|
|
393
691
|
}
|
|
@@ -396,6 +694,9 @@ export class TreeStore {
|
|
|
396
694
|
isVisible(node) {
|
|
397
695
|
if (node.status)
|
|
398
696
|
return this.indexOf(node) >= 0;
|
|
697
|
+
// A node the filter hides is no row; a shown node's parents are shown too.
|
|
698
|
+
if (!node.shown)
|
|
699
|
+
return false;
|
|
399
700
|
for (let parent = node.parent; parent; parent = parent.parent)
|
|
400
701
|
if (!parent.expanded)
|
|
401
702
|
return false;
|
|
@@ -682,7 +983,7 @@ export class TreeStore {
|
|
|
682
983
|
const removed = preOrder([node]);
|
|
683
984
|
// A parent that loses its last child becomes a leaf, checked when all of its items were.
|
|
684
985
|
if (parent && this.cascade && siblings.length === 1)
|
|
685
|
-
parent.checked = this.
|
|
986
|
+
parent.checked = this.wholeState(parent) === "checked";
|
|
686
987
|
siblings.splice(index, 1);
|
|
687
988
|
if (parent && parent.children.length === 0 && !this.canExpand(parent))
|
|
688
989
|
parent.expanded = false;
|
|
@@ -697,7 +998,166 @@ export class TreeStore {
|
|
|
697
998
|
this.rows = this.visibleRows(this.roots, null);
|
|
698
999
|
return removed;
|
|
699
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Writes an item where a change made elsewhere left it (see `TreeItemChange`): in place under the same parent (moved
|
|
1003
|
+
* among its siblings by `compare`), under its new parent when it moved - with what is loaded under it - and in for the
|
|
1004
|
+
* first time when it is new or back. Only where the parent's children are known: given, loaded, or known to be none.
|
|
1005
|
+
* Under a parent never opened it waits for that parent's first page, and one that sorts after the loaded children while
|
|
1006
|
+
* more are to come waits for its page. A lazy parent's `loaded` and `total` follow, so its next page skips the right
|
|
1007
|
+
* number, and so does the count in `hasChildrenKey` on the parent's own item, which its chevron goes by. Returns the
|
|
1008
|
+
* item's node where it is now, or `undefined` when this tree does not show it.
|
|
1009
|
+
*/
|
|
1010
|
+
placeItem(item, parentValue, options = {}) {
|
|
1011
|
+
const key = treeKey(read(item, this.valueKey));
|
|
1012
|
+
if (key === null)
|
|
1013
|
+
return undefined;
|
|
1014
|
+
const existing = this.byKey.get(key);
|
|
1015
|
+
if (existing?.draft)
|
|
1016
|
+
return undefined;
|
|
1017
|
+
const parentKey = treeKey(parentValue);
|
|
1018
|
+
const parent = parentKey === null ? null : this.byKey.get(parentKey);
|
|
1019
|
+
// Where it was: its node knows, else the change says - or nobody does, and then no count moves.
|
|
1020
|
+
let from;
|
|
1021
|
+
if (existing)
|
|
1022
|
+
from = existing.parent ? existing.parent.key : null;
|
|
1023
|
+
else if (options.before === false)
|
|
1024
|
+
from = false;
|
|
1025
|
+
else if (options.before !== undefined)
|
|
1026
|
+
from = treeKey(options.before);
|
|
1027
|
+
const moved = from !== undefined && from !== false && from !== parentKey;
|
|
1028
|
+
const valueBefore = this.value();
|
|
1029
|
+
const oldParent = existing ? existing.parent : null;
|
|
1030
|
+
let oldIndex = -1;
|
|
1031
|
+
if (existing) {
|
|
1032
|
+
const siblings = oldParent ? oldParent.children : this.roots;
|
|
1033
|
+
oldIndex = siblings.indexOf(existing);
|
|
1034
|
+
siblings.splice(oldIndex, 1);
|
|
1035
|
+
const pages = this.pagesOf(oldParent);
|
|
1036
|
+
if (pages && pages.loaded > 0)
|
|
1037
|
+
pages.loaded--;
|
|
1038
|
+
}
|
|
1039
|
+
let node;
|
|
1040
|
+
if (parent !== undefined && this.listsChildren(parent)) {
|
|
1041
|
+
const siblings = parent ? parent.children : this.roots;
|
|
1042
|
+
const pages = this.pagesOf(parent);
|
|
1043
|
+
const at = options.compare ? insertionIndex(siblings.map(each => each.item), item, options.compare) : existing && !moved ? Math.min(oldIndex, siblings.length) : siblings.length;
|
|
1044
|
+
if (at < siblings.length || !pages?.hasMore) {
|
|
1045
|
+
node = existing ?? this.createNode(item, key);
|
|
1046
|
+
node.parent = parent;
|
|
1047
|
+
siblings.splice(at, 0, node);
|
|
1048
|
+
if (pages)
|
|
1049
|
+
pages.loaded++;
|
|
1050
|
+
if (!existing) {
|
|
1051
|
+
node.checked = this.arrivalChecked(node);
|
|
1052
|
+
this.byKey.set(key, node);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (existing && !node) {
|
|
1057
|
+
// It went where this tree has not loaded: out, with everything under it.
|
|
1058
|
+
for (const each of preOrder([existing])) {
|
|
1059
|
+
this.byKey.delete(each.key);
|
|
1060
|
+
this.statusRows.delete(each.key);
|
|
1061
|
+
}
|
|
1062
|
+
existing.parent = null;
|
|
1063
|
+
}
|
|
1064
|
+
if (node) {
|
|
1065
|
+
node.item = item;
|
|
1066
|
+
node.value = read(item, this.valueKey);
|
|
1067
|
+
node.label = labelOf(item, this.labelKey);
|
|
1068
|
+
node.disabled = this.isDisabled(item);
|
|
1069
|
+
// Nothing loaded under it: what the item says about its children counts.
|
|
1070
|
+
if (this.lazy && node.children.length === 0 && node.pages && node.pages.status !== "loading")
|
|
1071
|
+
node.pages = this.newPages(item);
|
|
1072
|
+
for (const each of preOrder([node]))
|
|
1073
|
+
each.depth = each.parent ? each.parent.depth + 1 : 0;
|
|
1074
|
+
}
|
|
1075
|
+
if (moved)
|
|
1076
|
+
this.countChild(from, -1);
|
|
1077
|
+
if (moved || from === false)
|
|
1078
|
+
this.countChild(parentKey, 1);
|
|
1079
|
+
// A parent left with no children and nothing more to load closes, and loses its chevron.
|
|
1080
|
+
if (oldParent && oldParent.children.length === 0 && !this.canExpand(oldParent))
|
|
1081
|
+
oldParent.expanded = false;
|
|
1082
|
+
this.recount(preOrder(this.roots));
|
|
1083
|
+
// A checked value whose item went out of view waits in the value for it to come back, as after a refresh.
|
|
1084
|
+
const valueAfter = new Set(this.value().map(String));
|
|
1085
|
+
for (const value of valueBefore)
|
|
1086
|
+
if (!valueAfter.has(String(value)))
|
|
1087
|
+
this.pending.set(String(value), value);
|
|
1088
|
+
this.settleAll();
|
|
1089
|
+
this.rows = this.visibleRows(this.roots, null);
|
|
1090
|
+
return node;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Takes out an item a change made elsewhere took out of the tree - moved to the trash, deleted - with everything under
|
|
1094
|
+
* it. Its parent counts one fewer (its pages' `loaded` and `total`, the count on its item) and loses its chevron when
|
|
1095
|
+
* none are left. An item not loaded here still lowers its parent's counts when `parentValue` names the parent.
|
|
1096
|
+
* Returns the nodes taken out.
|
|
1097
|
+
*/
|
|
1098
|
+
removeItem(value, parentValue) {
|
|
1099
|
+
const key = treeKey(value);
|
|
1100
|
+
if (key === null)
|
|
1101
|
+
return [];
|
|
1102
|
+
const node = this.byKey.get(key);
|
|
1103
|
+
if (node?.draft)
|
|
1104
|
+
return [];
|
|
1105
|
+
let removed = [];
|
|
1106
|
+
if (node) {
|
|
1107
|
+
const parent = node.parent;
|
|
1108
|
+
const siblings = parent ? parent.children : this.roots;
|
|
1109
|
+
siblings.splice(siblings.indexOf(node), 1);
|
|
1110
|
+
const pages = this.pagesOf(parent);
|
|
1111
|
+
if (pages && pages.loaded > 0)
|
|
1112
|
+
pages.loaded--;
|
|
1113
|
+
removed = preOrder([node]);
|
|
1114
|
+
for (const each of removed) {
|
|
1115
|
+
this.byKey.delete(each.key);
|
|
1116
|
+
this.statusRows.delete(each.key);
|
|
1117
|
+
this.pending.delete(each.key);
|
|
1118
|
+
}
|
|
1119
|
+
node.parent = null;
|
|
1120
|
+
this.countChild(parent ? parent.key : null, -1);
|
|
1121
|
+
if (parent && parent.children.length === 0 && !this.canExpand(parent))
|
|
1122
|
+
parent.expanded = false;
|
|
1123
|
+
}
|
|
1124
|
+
else if (parentValue !== undefined) {
|
|
1125
|
+
this.countChild(treeKey(parentValue), -1);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
this.recount(preOrder(this.roots));
|
|
1131
|
+
this.settleAll();
|
|
1132
|
+
this.rows = this.visibleRows(this.roots, null);
|
|
1133
|
+
return removed;
|
|
1134
|
+
}
|
|
700
1135
|
// Internals
|
|
1136
|
+
/** Whether a parent's children are known here: given in the items, loaded (all or the first pages), or known to be none. */
|
|
1137
|
+
listsChildren(parent) {
|
|
1138
|
+
const pages = this.pagesOf(parent);
|
|
1139
|
+
return !pages || pages.loaded > 0 || pages.status === "ready";
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* One child more or fewer under a parent (`null`: the top level): its pages' total, and the count its own item
|
|
1143
|
+
* carries in `hasChildrenKey` - a new copy of the item. A parent never opened reads its children from the new count,
|
|
1144
|
+
* so one that has none left loses its chevron; one whose children were loaded knows already.
|
|
1145
|
+
*/
|
|
1146
|
+
countChild(parentKey, delta) {
|
|
1147
|
+
const parent = parentKey === null ? null : this.byKey.get(parentKey);
|
|
1148
|
+
if (parent === undefined)
|
|
1149
|
+
return;
|
|
1150
|
+
const pages = this.pagesOf(parent);
|
|
1151
|
+
if (pages?.total !== undefined)
|
|
1152
|
+
pages.total = Math.max(0, pages.total + delta);
|
|
1153
|
+
if (!parent || this.hasChildrenKey === undefined)
|
|
1154
|
+
return;
|
|
1155
|
+
const next = countedChildren(read(parent.item, this.hasChildrenKey), delta, pages?.total);
|
|
1156
|
+
if (next !== undefined)
|
|
1157
|
+
parent.item = { ...parent.item, [this.hasChildrenKey]: next };
|
|
1158
|
+
if (this.lazy && parent.children.length === 0 && parent.pages?.status === "idle")
|
|
1159
|
+
parent.pages = this.newPages(parent.item);
|
|
1160
|
+
}
|
|
701
1161
|
createNode(item, key) {
|
|
702
1162
|
return {
|
|
703
1163
|
uid: this.nextUid++,
|
|
@@ -721,6 +1181,12 @@ export class TreeStore {
|
|
|
721
1181
|
draft: false,
|
|
722
1182
|
pages: this.lazy ? this.newPages(item) : undefined,
|
|
723
1183
|
partial: false,
|
|
1184
|
+
shown: true,
|
|
1185
|
+
shownChildren: 0,
|
|
1186
|
+
shownLeaves: 1,
|
|
1187
|
+
shownChecked: 0,
|
|
1188
|
+
shownOpenLeaves: 0,
|
|
1189
|
+
shownOpenChecked: 0,
|
|
724
1190
|
};
|
|
725
1191
|
}
|
|
726
1192
|
/** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
|
|
@@ -769,6 +1235,8 @@ export class TreeStore {
|
|
|
769
1235
|
}
|
|
770
1236
|
for (let index = path.length - 1; index >= 0; index--)
|
|
771
1237
|
this.aggregate(path[index]);
|
|
1238
|
+
if (this.match !== null && added.length > 0)
|
|
1239
|
+
shownPositions(siblings);
|
|
772
1240
|
this.count(path, 1);
|
|
773
1241
|
this.count(added, 1);
|
|
774
1242
|
return added;
|
|
@@ -922,6 +1390,9 @@ export class TreeStore {
|
|
|
922
1390
|
stack.push(nodes[index]);
|
|
923
1391
|
while (stack.length > 0) {
|
|
924
1392
|
const node = stack.pop();
|
|
1393
|
+
// Hidden by the filter, with everything under it.
|
|
1394
|
+
if (!node.shown)
|
|
1395
|
+
continue;
|
|
925
1396
|
list.push(node);
|
|
926
1397
|
if (!node.expanded)
|
|
927
1398
|
continue;
|
|
@@ -971,6 +1442,12 @@ export class TreeStore {
|
|
|
971
1442
|
openChecked: 0,
|
|
972
1443
|
draft: false,
|
|
973
1444
|
partial: false,
|
|
1445
|
+
shown: true,
|
|
1446
|
+
shownChildren: 0,
|
|
1447
|
+
shownLeaves: 0,
|
|
1448
|
+
shownChecked: 0,
|
|
1449
|
+
shownOpenLeaves: 0,
|
|
1450
|
+
shownOpenChecked: 0,
|
|
974
1451
|
};
|
|
975
1452
|
this.statusRows.set(parentKey, row);
|
|
976
1453
|
}
|
|
@@ -1006,6 +1483,12 @@ export class TreeStore {
|
|
|
1006
1483
|
}
|
|
1007
1484
|
for (let index = ordered.length - 1; index >= 0; index--)
|
|
1008
1485
|
this.aggregate(ordered[index]);
|
|
1486
|
+
// Under a filter a row's place among its siblings (aria-posinset) counts the shown ones only.
|
|
1487
|
+
if (this.match !== null) {
|
|
1488
|
+
shownPositions(this.roots);
|
|
1489
|
+
for (const node of ordered)
|
|
1490
|
+
shownPositions(node.children);
|
|
1491
|
+
}
|
|
1009
1492
|
this.checkedNodes = 0;
|
|
1010
1493
|
this.openNodes = 0;
|
|
1011
1494
|
this.openCheckedNodes = 0;
|
|
@@ -1016,6 +1499,7 @@ export class TreeStore {
|
|
|
1016
1499
|
* does for every node, and what a page of children does for each node on its parent's path.
|
|
1017
1500
|
*/
|
|
1018
1501
|
aggregate(node) {
|
|
1502
|
+
this.counted++;
|
|
1019
1503
|
const pages = node.pages;
|
|
1020
1504
|
const own = pages !== undefined && (pages.status !== "ready" || pages.hasMore);
|
|
1021
1505
|
if (node.children.length === 0) {
|
|
@@ -1027,6 +1511,14 @@ export class TreeStore {
|
|
|
1027
1511
|
node.checkedLeaves = node.checked ? 1 : 0;
|
|
1028
1512
|
node.openLeaves = fixed ? 0 : 1;
|
|
1029
1513
|
node.openChecked = fixed ? 0 : node.checkedLeaves;
|
|
1514
|
+
// A draft being named shows whatever the filter says: its name box must not vanish.
|
|
1515
|
+
const matched = this.match === null || node.draft || this.match(node.item);
|
|
1516
|
+
node.shown = matched || own;
|
|
1517
|
+
node.shownChildren = 0;
|
|
1518
|
+
node.shownLeaves = matched ? 1 : 0;
|
|
1519
|
+
node.shownChecked = matched ? node.checkedLeaves : 0;
|
|
1520
|
+
node.shownOpenLeaves = matched ? node.openLeaves : 0;
|
|
1521
|
+
node.shownOpenChecked = matched ? node.openChecked : 0;
|
|
1030
1522
|
return;
|
|
1031
1523
|
}
|
|
1032
1524
|
let leaves = 0;
|
|
@@ -1034,7 +1526,13 @@ export class TreeStore {
|
|
|
1034
1526
|
let openLeaves = 0;
|
|
1035
1527
|
let openChecked = 0;
|
|
1036
1528
|
let allLocked = true;
|
|
1529
|
+
let shownLocked = true;
|
|
1037
1530
|
let partial = own;
|
|
1531
|
+
let shownChildren = 0;
|
|
1532
|
+
let shownLeaves = 0;
|
|
1533
|
+
let shownChecked = 0;
|
|
1534
|
+
let shownOpenLeaves = 0;
|
|
1535
|
+
let shownOpenChecked = 0;
|
|
1038
1536
|
for (const child of node.children) {
|
|
1039
1537
|
leaves += child.leaves;
|
|
1040
1538
|
checked += child.checkedLeaves;
|
|
@@ -1042,14 +1540,29 @@ export class TreeStore {
|
|
|
1042
1540
|
openChecked += child.openChecked;
|
|
1043
1541
|
allLocked &&= child.locked;
|
|
1044
1542
|
partial ||= child.partial;
|
|
1543
|
+
if (!child.shown)
|
|
1544
|
+
continue;
|
|
1545
|
+
shownChildren++;
|
|
1546
|
+
shownLeaves += child.shownLeaves;
|
|
1547
|
+
shownChecked += child.shownChecked;
|
|
1548
|
+
shownOpenLeaves += child.shownOpenLeaves;
|
|
1549
|
+
shownOpenChecked += child.shownOpenChecked;
|
|
1550
|
+
shownLocked &&= child.locked;
|
|
1045
1551
|
}
|
|
1046
1552
|
node.leaves = leaves;
|
|
1047
1553
|
node.checkedLeaves = checked;
|
|
1048
1554
|
node.openLeaves = openLeaves;
|
|
1049
1555
|
node.openChecked = openChecked;
|
|
1050
1556
|
node.partial = partial;
|
|
1051
|
-
|
|
1052
|
-
node.
|
|
1557
|
+
node.shown = this.match === null || shownChildren > 0 || own;
|
|
1558
|
+
node.shownChildren = shownChildren;
|
|
1559
|
+
node.shownLeaves = shownLeaves;
|
|
1560
|
+
node.shownChecked = shownChecked;
|
|
1561
|
+
node.shownOpenLeaves = shownOpenLeaves;
|
|
1562
|
+
node.shownOpenChecked = shownOpenChecked;
|
|
1563
|
+
// A parent's box follows its items only with cascade; without it, a parent stays free. Under a filter it follows the
|
|
1564
|
+
// items shown: a parent whose shown items are all locked has nothing a check could change.
|
|
1565
|
+
node.locked = node.disabledPath || (this.cascade && (this.match !== null && shownChildren > 0 ? shownLocked : allLocked));
|
|
1053
1566
|
}
|
|
1054
1567
|
/** Sets every open leaf under a node (the node itself for a leaf), updating counts up the path of each leaf that changes. */
|
|
1055
1568
|
setLeaves(node, checked) {
|
|
@@ -1057,8 +1570,9 @@ export class TreeStore {
|
|
|
1057
1570
|
const stack = [node];
|
|
1058
1571
|
while (stack.length > 0) {
|
|
1059
1572
|
const current = stack.pop();
|
|
1060
|
-
// A subtree that is locked, or already all checked or all unchecked, has nothing to change
|
|
1061
|
-
|
|
1573
|
+
// A subtree that is locked, or whose shown leaves are already all checked or all unchecked, has nothing to change:
|
|
1574
|
+
// a leaf the filter hides counts in no `shown` number, so it is never reached.
|
|
1575
|
+
if (current.locked || current.shownOpenChecked === (checked ? current.shownOpenLeaves : 0))
|
|
1062
1576
|
continue;
|
|
1063
1577
|
if (current.children.length > 0) {
|
|
1064
1578
|
for (const child of current.children)
|
|
@@ -1070,6 +1584,8 @@ export class TreeStore {
|
|
|
1070
1584
|
for (let path = current; path; path = path.parent) {
|
|
1071
1585
|
path.checkedLeaves += step;
|
|
1072
1586
|
path.openChecked += step;
|
|
1587
|
+
path.shownChecked += step;
|
|
1588
|
+
path.shownOpenChecked += step;
|
|
1073
1589
|
}
|
|
1074
1590
|
changed = true;
|
|
1075
1591
|
}
|
|
@@ -1084,10 +1600,16 @@ export class TreeStore {
|
|
|
1084
1600
|
if (!node.locked)
|
|
1085
1601
|
this.openCheckedNodes += step;
|
|
1086
1602
|
if (node.children.length === 0) {
|
|
1603
|
+
const shown = node.shownLeaves === 1;
|
|
1087
1604
|
for (let path = node; path; path = path.parent) {
|
|
1088
1605
|
path.checkedLeaves += step;
|
|
1089
1606
|
if (!node.locked)
|
|
1090
1607
|
path.openChecked += step;
|
|
1608
|
+
if (!shown)
|
|
1609
|
+
continue;
|
|
1610
|
+
path.shownChecked += step;
|
|
1611
|
+
if (!node.locked)
|
|
1612
|
+
path.shownOpenChecked += step;
|
|
1091
1613
|
}
|
|
1092
1614
|
}
|
|
1093
1615
|
return true;
|
|
@@ -1152,6 +1674,13 @@ function valueMap(values) {
|
|
|
1152
1674
|
}
|
|
1153
1675
|
return map;
|
|
1154
1676
|
}
|
|
1677
|
+
/** Numbers siblings among the shown ones (`position`), as a filtered tree's rows are read out. */
|
|
1678
|
+
function shownPositions(siblings) {
|
|
1679
|
+
let position = 0;
|
|
1680
|
+
for (const node of siblings)
|
|
1681
|
+
if (node.shown)
|
|
1682
|
+
node.position = position++;
|
|
1683
|
+
}
|
|
1155
1684
|
/** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
|
|
1156
1685
|
export function preOrder(nodes) {
|
|
1157
1686
|
const list = [];
|