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.
Files changed (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. 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" : node.checkedLeaves > 0 ? "mixed" : "unchecked";
257
- if (node.checkedLeaves === 0)
517
+ return this.pending.has(node.key) ? "checked" : checked > 0 ? "mixed" : "unchecked";
518
+ if (checked === 0)
258
519
  return "unchecked";
259
- return node.checkedLeaves === node.leaves ? "checked" : "mixed";
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.openChecked < node.openLeaves);
624
+ return this.setChecked(node, node.shownOpenChecked < node.shownOpenLeaves);
341
625
  }
342
- /** Checks or unchecks a node; with `cascade`, every item under it that is not locked. Returns whether anything changed. */
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 item is locked. */
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.openLeaves;
365
- checked += root.openChecked;
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 lazy tree with `cascade`,
379
- * a node whose items are not all loaded is passed over and the loaded parts under it are changed; values whose
380
- * items are not loaded stay as they are.
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.state(parent) === "checked";
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
- // A parent's box follows its items only with cascade; without it, a parent stays free.
1052
- node.locked = node.disabledPath || (this.cascade && allLocked);
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
- if (current.locked || current.openChecked === (checked ? current.openLeaves : 0))
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 = [];