nexus-shared 1.1.20 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +903 -0
  3. package/dist/Client.Index.d.ts +44 -0
  4. package/dist/Client.Index.js +49 -0
  5. package/dist/Components/Documents/Button.d.ts +29 -0
  6. package/dist/Components/Documents/Button.js +11 -0
  7. package/dist/Components/Documents/Menu.d.ts +102 -0
  8. package/dist/Components/Documents/Menu.js +238 -0
  9. package/dist/Components/Documents/SplitButton.d.ts +64 -0
  10. package/dist/Components/Documents/SplitButton.js +127 -0
  11. package/dist/Components/Documents/TabButtons.d.ts +62 -0
  12. package/dist/Components/Documents/TabButtons.js +19 -0
  13. package/dist/Components/Forms/ApiForm.d.ts +21 -0
  14. package/dist/Components/Forms/ApiForm.js +99 -0
  15. package/dist/Components/Forms/Crud.d.ts +17 -0
  16. package/dist/Components/Forms/Crud.js +556 -0
  17. package/dist/Components/Forms/CrudParts.d.ts +15 -0
  18. package/dist/Components/Forms/CrudParts.js +36 -0
  19. package/dist/Components/Forms/Form.d.ts +52 -0
  20. package/dist/Components/Forms/Form.js +319 -0
  21. package/dist/Components/Forms/SubmitForm.d.ts +65 -0
  22. package/dist/Components/Forms/SubmitForm.js +198 -0
  23. package/dist/Components/Inputs/Calendar.d.ts +69 -0
  24. package/dist/Components/Inputs/Calendar.js +258 -0
  25. package/dist/Components/Inputs/CheckBox.d.ts +53 -0
  26. package/dist/Components/Inputs/CheckBox.js +87 -0
  27. package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
  28. package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
  29. package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
  30. package/dist/Components/Inputs/ChoiceField.js +169 -0
  31. package/dist/Components/Inputs/DatePicker.d.ts +54 -0
  32. package/dist/Components/Inputs/DatePicker.js +246 -0
  33. package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
  34. package/dist/Components/Inputs/DateRangePicker.js +283 -0
  35. package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
  36. package/dist/Components/Inputs/DateTimePicker.js +399 -0
  37. package/dist/Components/Inputs/Dropdown.d.ts +117 -0
  38. package/dist/Components/Inputs/Dropdown.js +839 -0
  39. package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
  40. package/dist/Components/Inputs/DropdownParts.js +102 -0
  41. package/dist/Components/Inputs/EditorShell.d.ts +45 -0
  42. package/dist/Components/Inputs/EditorShell.js +214 -0
  43. package/dist/Components/Inputs/Field.d.ts +53 -0
  44. package/dist/Components/Inputs/Field.js +16 -0
  45. package/dist/Components/Inputs/GroupForm.d.ts +20 -0
  46. package/dist/Components/Inputs/GroupForm.js +170 -0
  47. package/dist/Components/Inputs/InputField.d.ts +59 -0
  48. package/dist/Components/Inputs/InputField.js +169 -0
  49. package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
  50. package/dist/Components/Inputs/InputRenderer.js +99 -0
  51. package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
  52. package/dist/Components/Inputs/MarkdownEditor.js +289 -0
  53. package/dist/Components/Inputs/NumberBox.d.ts +53 -0
  54. package/dist/Components/Inputs/NumberBox.js +228 -0
  55. package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
  56. package/dist/Components/Inputs/RadioGroup.js +43 -0
  57. package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
  58. package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
  59. package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
  60. package/dist/Components/Inputs/RichTextEditor.js +1170 -0
  61. package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
  62. package/dist/Components/Inputs/RichTextParts.js +157 -0
  63. package/dist/Components/Inputs/RowsInput.d.ts +104 -0
  64. package/dist/Components/Inputs/RowsInput.js +259 -0
  65. package/dist/Components/Inputs/Slider.d.ts +87 -0
  66. package/dist/Components/Inputs/Slider.js +216 -0
  67. package/dist/Components/Inputs/TabularForm.d.ts +23 -0
  68. package/dist/Components/Inputs/TabularForm.js +283 -0
  69. package/dist/Components/Inputs/TextArea.d.ts +43 -0
  70. package/dist/Components/Inputs/TextArea.js +31 -0
  71. package/dist/Components/Inputs/TextBox.d.ts +52 -0
  72. package/dist/Components/Inputs/TextBox.js +58 -0
  73. package/dist/Components/Inputs/TextField.d.ts +31 -0
  74. package/dist/Components/Inputs/TextField.js +35 -0
  75. package/dist/Components/Inputs/TimePanel.d.ts +34 -0
  76. package/dist/Components/Inputs/TimePanel.js +288 -0
  77. package/dist/Components/Inputs/TimePicker.d.ts +54 -0
  78. package/dist/Components/Inputs/TimePicker.js +239 -0
  79. package/dist/Components/Inputs/TreeView.d.ts +160 -0
  80. package/dist/Components/Inputs/TreeView.js +947 -0
  81. package/dist/Components/Layouts/MessageBox.d.ts +71 -0
  82. package/dist/Components/Layouts/MessageBox.js +104 -0
  83. package/dist/Components/Layouts/Popup.d.ts +92 -0
  84. package/dist/Components/Layouts/Popup.js +376 -0
  85. package/dist/Components/Layouts/PopupContext.d.ts +2 -0
  86. package/dist/Components/Layouts/PopupContext.js +4 -0
  87. package/dist/Components/Layouts/PopupDock.d.ts +22 -0
  88. package/dist/Components/Layouts/PopupDock.js +121 -0
  89. package/dist/Components/Layouts/PopupHost.d.ts +56 -0
  90. package/dist/Components/Layouts/PopupHost.js +163 -0
  91. package/dist/Components/Layouts/PopupRules.d.ts +30 -0
  92. package/dist/Components/Layouts/PopupRules.js +63 -0
  93. package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
  94. package/dist/Components/Layouts/ThemePicker.js +22 -0
  95. package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
  96. package/dist/Components/Layouts/ThemeScript.js +13 -0
  97. package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
  98. package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
  99. package/dist/Components/Layouts/Toaster.d.ts +76 -0
  100. package/dist/Components/Layouts/Toaster.js +172 -0
  101. package/dist/Components/Viewers/DataTable.d.ts +25 -0
  102. package/dist/Components/Viewers/DataTable.js +542 -0
  103. package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
  104. package/dist/Components/Viewers/DataTableParts.js +145 -0
  105. package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
  106. package/dist/Components/Viewers/MarkdownView.js +79 -0
  107. package/dist/Components/Viewers/RichTextView.d.ts +18 -0
  108. package/dist/Components/Viewers/RichTextView.js +17 -0
  109. package/dist/Helpers/AnimationHelpers.d.ts +6 -0
  110. package/dist/Helpers/AnimationHelpers.js +35 -0
  111. package/dist/Helpers/ApiClient.d.ts +74 -0
  112. package/dist/Helpers/ApiClient.js +197 -0
  113. package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
  114. package/dist/Helpers/ApiFormHelpers.js +56 -0
  115. package/dist/Helpers/ApiModules.d.ts +135 -0
  116. package/dist/Helpers/ApiModules.js +194 -0
  117. package/dist/Helpers/ApiResponses.d.ts +41 -0
  118. package/dist/Helpers/ApiResponses.js +159 -0
  119. package/dist/Helpers/ApiRoutes.d.ts +150 -0
  120. package/dist/Helpers/ApiRoutes.js +191 -0
  121. package/dist/Helpers/ApiToasts.d.ts +21 -0
  122. package/dist/Helpers/ApiToasts.js +22 -0
  123. package/dist/Helpers/ClassHelpers.d.ts +3 -0
  124. package/dist/Helpers/ClassHelpers.js +4 -0
  125. package/dist/Helpers/CrudBackend.d.ts +56 -0
  126. package/dist/Helpers/CrudBackend.js +119 -0
  127. package/dist/Helpers/CrudHelpers.d.ts +76 -0
  128. package/dist/Helpers/CrudHelpers.js +202 -0
  129. package/dist/Helpers/DateHelpers.d.ts +88 -0
  130. package/dist/Helpers/DateHelpers.js +363 -0
  131. package/dist/Helpers/DatePreferences.d.ts +14 -0
  132. package/dist/Helpers/DatePreferences.js +94 -0
  133. package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
  134. package/dist/Helpers/DateTimeHelpers.js +286 -0
  135. package/dist/Helpers/DragHelpers.d.ts +129 -0
  136. package/dist/Helpers/DragHelpers.js +281 -0
  137. package/dist/Helpers/DropdownHelpers.d.ts +56 -0
  138. package/dist/Helpers/DropdownHelpers.js +180 -0
  139. package/dist/Helpers/FormDrafts.d.ts +18 -0
  140. package/dist/Helpers/FormDrafts.js +76 -0
  141. package/dist/Helpers/FormStore.d.ts +231 -0
  142. package/dist/Helpers/FormStore.js +734 -0
  143. package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
  144. package/dist/Helpers/InputParamsHelpers.js +388 -0
  145. package/dist/Helpers/MarkdownEditing.d.ts +27 -0
  146. package/dist/Helpers/MarkdownEditing.js +250 -0
  147. package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
  148. package/dist/Helpers/MarkdownHelpers.js +775 -0
  149. package/dist/Helpers/MessageBuilder.d.ts +74 -0
  150. package/dist/Helpers/MessageBuilder.js +254 -0
  151. package/dist/Helpers/NepaliCalendar.d.ts +25 -0
  152. package/dist/Helpers/NepaliCalendar.js +107 -0
  153. package/dist/Helpers/NumberHelpers.d.ts +71 -0
  154. package/dist/Helpers/NumberHelpers.js +202 -0
  155. package/dist/Helpers/Permissions.d.ts +62 -0
  156. package/dist/Helpers/Permissions.js +133 -0
  157. package/dist/Helpers/PopoverHelpers.d.ts +43 -0
  158. package/dist/Helpers/PopoverHelpers.js +91 -0
  159. package/dist/Helpers/RichTextHelpers.d.ts +77 -0
  160. package/dist/Helpers/RichTextHelpers.js +1072 -0
  161. package/dist/Helpers/RowsStore.d.ts +160 -0
  162. package/dist/Helpers/RowsStore.js +584 -0
  163. package/dist/Helpers/TableColumns.d.ts +40 -0
  164. package/dist/Helpers/TableColumns.js +254 -0
  165. package/dist/Helpers/TableExport.d.ts +30 -0
  166. package/dist/Helpers/TableExport.js +97 -0
  167. package/dist/Helpers/TableHelpers.d.ts +102 -0
  168. package/dist/Helpers/TableHelpers.js +281 -0
  169. package/dist/Helpers/TableStore.d.ts +142 -0
  170. package/dist/Helpers/TableStore.js +530 -0
  171. package/dist/Helpers/ThemeHelpers.d.ts +15 -0
  172. package/dist/Helpers/ThemeHelpers.js +16 -0
  173. package/dist/Helpers/TimeHelpers.d.ts +73 -0
  174. package/dist/Helpers/TimeHelpers.js +253 -0
  175. package/dist/Helpers/TreeStore.d.ts +188 -0
  176. package/dist/Helpers/TreeStore.js +577 -0
  177. package/dist/Helpers/ValidationHelpers.d.ts +124 -0
  178. package/dist/Helpers/ValidationHelpers.js +301 -0
  179. package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
  180. package/dist/Interfaces/ApiInterfaces.js +24 -0
  181. package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
  182. package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
  183. package/dist/Interfaces/DateInterfaces.d.ts +154 -0
  184. package/dist/Interfaces/DateInterfaces.js +29 -0
  185. package/dist/Interfaces/FormInterfaces.d.ts +352 -0
  186. package/dist/Interfaces/FormInterfaces.js +1 -0
  187. package/dist/Interfaces/InputInterfaces.d.ts +141 -0
  188. package/dist/Interfaces/InputInterfaces.js +1 -0
  189. package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
  190. package/dist/Interfaces/MessageInterfaces.js +5 -0
  191. package/dist/Interfaces/TableInterfaces.d.ts +291 -0
  192. package/dist/Interfaces/TableInterfaces.js +1 -0
  193. package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
  194. package/dist/Interfaces/ThemeInterfaces.js +17 -0
  195. package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
  196. package/dist/Interfaces/TypeInterfaces.js +1 -0
  197. package/dist/Server.Index.d.ts +2 -0
  198. package/dist/Server.Index.js +5 -0
  199. package/dist/Services/ApiProxy.d.ts +32 -0
  200. package/dist/Services/ApiProxy.js +111 -0
  201. package/dist/Services/BrowserApi.d.ts +78 -0
  202. package/dist/Services/BrowserApi.js +111 -0
  203. package/dist/Services/ServerApi.d.ts +49 -0
  204. package/dist/Services/ServerApi.js +75 -0
  205. package/dist/Services/ThemeService.d.ts +17 -0
  206. package/dist/Services/ThemeService.js +83 -0
  207. package/dist/Shared.Index.d.ts +43 -0
  208. package/dist/Shared.Index.js +47 -0
  209. package/package.json +82 -33
  210. package/src/Styles/Nexus.Base.css +70 -0
  211. package/src/Styles/Nexus.Button.css +151 -0
  212. package/src/Styles/Nexus.Calendar.css +275 -0
  213. package/src/Styles/Nexus.Choice.css +345 -0
  214. package/src/Styles/Nexus.Crud.css +84 -0
  215. package/src/Styles/Nexus.DateTime.css +81 -0
  216. package/src/Styles/Nexus.Dropdown.css +774 -0
  217. package/src/Styles/Nexus.Editor.css +297 -0
  218. package/src/Styles/Nexus.Form.css +298 -0
  219. package/src/Styles/Nexus.Index.css +34 -0
  220. package/src/Styles/Nexus.Input.css +637 -0
  221. package/src/Styles/Nexus.Markdown.css +265 -0
  222. package/src/Styles/Nexus.Menu.css +249 -0
  223. package/src/Styles/Nexus.Popup.css +731 -0
  224. package/src/Styles/Nexus.RichText.css +463 -0
  225. package/src/Styles/Nexus.Rows.css +566 -0
  226. package/src/Styles/Nexus.Slider.css +278 -0
  227. package/src/Styles/Nexus.Tab.Buttons.css +364 -0
  228. package/src/Styles/Nexus.Table.css +1014 -0
  229. package/src/Styles/Nexus.Theme.Picker.css +159 -0
  230. package/src/Styles/Nexus.Themes.css +194 -0
  231. package/src/Styles/Nexus.Time.css +147 -0
  232. package/src/Styles/Nexus.Toast.css +265 -0
  233. package/src/Styles/Nexus.Tokens.css +72 -0
  234. package/src/Styles/Nexus.Tree.css +435 -0
  235. package/src/api-services/authentication-service.tsx +0 -23
  236. package/src/api-services/preference-service.tsx +0 -5
  237. package/src/api-services/system-service.tsx +0 -34
  238. package/src/client.ts +0 -31
  239. package/src/components/documents/button.tsx +0 -142
  240. package/src/components/documents/icon-box.tsx +0 -93
  241. package/src/components/documents/page-title.tsx +0 -7
  242. package/src/components/documents/tab-button.tsx +0 -172
  243. package/src/components/documents/tag.tsx +0 -30
  244. package/src/components/index.js +0 -0
  245. package/src/components/inputs/checkbox-input.tsx +0 -66
  246. package/src/components/inputs/input-box.tsx +0 -48
  247. package/src/components/inputs/input-element.tsx +0 -65
  248. package/src/components/inputs/input-form.tsx +0 -164
  249. package/src/components/inputs/input.tsx +0 -181
  250. package/src/components/inputs/number-input.tsx +0 -108
  251. package/src/components/inputs/radiobox-input.tsx +0 -53
  252. package/src/components/inputs/textarea-input.tsx +0 -47
  253. package/src/components/inputs/textbox-input.tsx +0 -45
  254. package/src/components/layouts/global-dialogbox.tsx +0 -431
  255. package/src/components/layouts/global-layout.tsx +0 -59
  256. package/src/components/layouts/layout-helpers.tsx +0 -20
  257. package/src/components/layouts/panels/user-panel.tsx +0 -112
  258. package/src/components/layouts/utility-menu.tsx +0 -49
  259. package/src/components/panels/theme-panel.tsx +0 -46
  260. package/src/helpers/bitwise-helpers.tsx +0 -11
  261. package/src/helpers/browser-helpers.tsx +0 -156
  262. package/src/helpers/datasource-helpers.tsx +0 -99
  263. package/src/helpers/element-helpers.tsx +0 -57
  264. package/src/helpers/input-helpers.tsx +0 -74
  265. package/src/helpers/string-helpers.tsx +0 -28
  266. package/src/helpers/utility-helpers.tsx +0 -45
  267. package/src/helpers/validation-helpers.tsx +0 -302
  268. package/src/index.ts +0 -26
  269. package/src/interface.ts +0 -19
  270. package/src/interfaces/auth-token-interfaces.tsx +0 -6
  271. package/src/interfaces/browser-interfaces.tsx +0 -36
  272. package/src/interfaces/button-interfaces.tsx +0 -63
  273. package/src/interfaces/datasource-interfaces.tsx +0 -22
  274. package/src/interfaces/datatable-interfaces.tsx +0 -25
  275. package/src/interfaces/dialogbox-interfaces.tsx +0 -5
  276. package/src/interfaces/exception-interfaces.tsx +0 -99
  277. package/src/interfaces/http-interfaces.tsx +0 -129
  278. package/src/interfaces/icon-interfaces.tsx +0 -129
  279. package/src/interfaces/input-interfaces.tsx +0 -410
  280. package/src/interfaces/layout-interfaces.tsx +0 -191
  281. package/src/interfaces/menu-interfaces.tsx +0 -49
  282. package/src/interfaces/message-interfaces.tsx +0 -32
  283. package/src/interfaces/permission-interfaces.tsx +0 -9
  284. package/src/interfaces/storage-interfaces.tsx +0 -5
  285. package/src/interfaces/system-interfaces.tsx +0 -22
  286. package/src/interfaces/theme-interfaces.tsx +0 -123
  287. package/src/interfaces/type-interfaces.tsx +0 -28
  288. package/src/interfaces/user-interfaces.tsx +0 -47
  289. package/src/nexus-client.tsx +0 -21
  290. package/src/nexus.environments.tsx +0 -66
  291. package/src/proxy-api/proxy-backend.tsx +0 -60
  292. package/src/proxy-api/proxy-constants.tsx +0 -28
  293. package/src/proxy-api/proxy-helpers.tsx +0 -91
  294. package/src/proxy-api-client.tsx +0 -1
  295. package/src/proxy-api-server.tsx +0 -2
  296. package/src/services/http-client-services.tsx +0 -75
  297. package/src/services/http-services.tsx +0 -158
  298. package/src/services/loader-service.tsx +0 -185
  299. package/src/services/localstorage-service.tsx +0 -119
  300. package/src/services/message-services.tsx +0 -383
  301. package/src/services/theme-service.tsx +0 -161
  302. package/src/services/user-services.tsx +0 -10
  303. package/src/sso-config/auth-token-validation.ts +0 -16
  304. package/src/sso-config/callback-route.tsx +0 -35
  305. package/src/sso-config/cookie-encryption.ts +0 -51
  306. package/src/sso-config/cookie-helpers.tsx +0 -102
  307. package/src/sso-config/forgot-password-route.tsx +0 -36
  308. package/src/sso-config/oauth-callback-state.ts +0 -61
  309. package/src/sso-config/pkce-helpers.ts +0 -9
  310. package/src/sso-config/provider-complete-route.tsx +0 -18
  311. package/src/sso-config/redirect-context-actions.tsx +0 -45
  312. package/src/sso-config/redirect-context-helpers.tsx +0 -159
  313. package/src/sso-config/redirect-context-persist.tsx +0 -18
  314. package/src/sso-config/redirect-context-route.tsx +0 -45
  315. package/src/sso-config/refresh-route.tsx +0 -23
  316. package/src/sso-config/sign-in-route.tsx +0 -74
  317. package/src/sso-config/sign-out-route.tsx +0 -57
  318. package/src/sso-config/sign-up-route.tsx +0 -57
  319. package/src/sso-config/sso-backend-flow.ts +0 -38
  320. package/src/sso-config/sso-complete-route.tsx +0 -50
  321. package/src/sso-config/sso-config.ts +0 -59
  322. package/src/sso-config/sso-interfaces.tsx +0 -99
  323. package/src/sso-config/sso-response-helpers.tsx +0 -20
  324. package/src/sso-server.tsx +0 -21
  325. package/src/styles/nexus.animation.css +0 -269
  326. package/src/styles/nexus.core.css +0 -119
  327. package/src/styles/nexus.dialog.css +0 -144
  328. package/src/styles/nexus.icon.css +0 -51
  329. package/src/styles/nexus.input.css +0 -207
  330. package/src/styles/nexus.loader.css +0 -11
  331. package/src/styles/nexus.logic.css +0 -47
  332. package/src/styles/nexus.utility.css +0 -347
@@ -0,0 +1,947 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Icon, iconAlertCircle, iconCheck, iconChevronRight, iconChevronsDown, iconChevronsUp, iconLoader2, iconLock, iconPencil, iconPlus, iconTrash, iconX, InlineIcon, } from "nexus-icons";
4
+ import { isValidElement, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
5
+ import { afterAnimation } from "../../Helpers/AnimationHelpers.js";
6
+ import { cx } from "../../Helpers/ClassHelpers.js";
7
+ import { TREE_DEFAULT_KEYS, TreeStore, } from "../../Helpers/TreeStore.js";
8
+ import { Button } from "../Documents/Button.js";
9
+ import { ContextMenu } from "../Documents/Menu.js";
10
+ import { messageBox } from "../Layouts/MessageBox.js";
11
+ import { CheckBox } from "./CheckBox.js";
12
+ import { hasContent } from "./Field.js";
13
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
14
+ /** Row heights in pixels, the same as `--nx-tree-row` in Nexus.Tree.css. Fixed heights let the tree place any row without rendering the rows above it. */
15
+ const ROW_HEIGHT = { sm: 28, md: 32, lg: 38 };
16
+ /** Space above the first row and below the last, in pixels (`--nx-tree-pad`). */
17
+ const PAD = 4;
18
+ /** Rows rendered past each edge of the viewport, so a short scroll finds its rows already there. */
19
+ const OVERSCAN = 12;
20
+ /** Letters typed within this time, in milliseconds, add up to one search for a row. */
21
+ const TYPEAHEAD_DELAY = 600;
22
+ const SKELETON_WIDTHS = ["42%", "58%", "36%", "64%", "48%"];
23
+ let draftCounter = 0;
24
+ /** A value for an item added in the browser, until a server gives it one. */
25
+ function newTreeValue() {
26
+ const uuid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : null;
27
+ return uuid ?? `new-${Date.now().toString(36)}-${(++draftCounter).toString(36)}`;
28
+ }
29
+ const errorText = (error) => (error instanceof Error && error.message ? error.message : typeof error === "string" && error ? error : "Something went wrong.");
30
+ const isItem = (value) => typeof value === "object" && value !== null;
31
+ const sameItems = (a, b) => a === b || (a.length === b.length && a.every((item, index) => item === b[index]));
32
+ /**
33
+ * A tree of items from a flat list: a permission tree (controller > menu > action) with checkboxes that cascade and
34
+ * items the user may not change, or a tree of categories to add, rename, and delete. Only the rows in view are
35
+ * rendered, so collapsed items never reach the page, and 100,000 expanded items scroll like 40. Expanding and
36
+ * collapsing slide open and closed (at once with reduced motion).
37
+ *
38
+ * Keys: arrows move and expand, Home and End jump, letters find a row, Space checks, Enter opens, `*` expands the
39
+ * siblings, F2 renames, Delete deletes, Insert adds under the focused row, and Shift+F10 opens the actions menu.
40
+ *
41
+ * @example
42
+ * <TreeView label="Permissions" items={permissions} checkable name="actionIds" defaultExpanded={1} isItemDisabled={item => !item.canAssign} />
43
+ */
44
+ export function TreeView(props) {
45
+ const { items, valueKey = TREE_DEFAULT_KEYS.value, parentKey = TREE_DEFAULT_KEYS.parent, labelKey = TREE_DEFAULT_KEYS.label, descriptionKey = "description", iconKey = "icon", isItemDisabled, lockedMessage = "You do not have permission to change this item.", label, hint, error, size = "md", checkable = false, cascade = true, valueMode = "leaves", value, defaultValue, onValueChange, name, form, counts = true, defaultExpanded, creatable = false, editable = false, deletable = false, canChange, onCreate, onUpdate, onDelete, onItemsChange, confirmDelete = true, maxLabelLength = 100, onItemClick, loading = false, emptyMessage = "No items", toolbar = true, height, maxHeight = 420, readOnly = false, readOnlyMessage = "Read-only: this tree cannot be changed.", disabled = false, id, className, style, "aria-label": ariaLabel, ref, } = props;
46
+ const generatedId = useId();
47
+ const treeId = id ?? generatedId;
48
+ const labelId = `${treeId}-label`;
49
+ const messageId = `${treeId}-message`;
50
+ const rowHeight = ROW_HEIGHT[size];
51
+ // The store holds the tree outside React. It is built again only when a key or a checking rule changes.
52
+ const disabledRef = useRef(isItemDisabled);
53
+ disabledRef.current = isItemDisabled;
54
+ const config = [valueKey, parentKey, labelKey, cascade, valueMode].join("");
55
+ const holderRef = useRef(null);
56
+ // Whether the user (or the handle) changed the checks since the items came, so new items keep them.
57
+ const touchedRef = useRef(false);
58
+ if (holderRef.current?.config !== config) {
59
+ // A new rule (another value mode, say) keeps what was checked and expanded.
60
+ const previous = holderRef.current?.store;
61
+ const store = new TreeStore(items, {
62
+ valueKey,
63
+ parentKey,
64
+ labelKey,
65
+ cascade,
66
+ valueMode,
67
+ isItemDisabled: item => (disabledRef.current ? disabledRef.current(item) : read(item, "disabled") === true || read(item, "isDisabled") === true),
68
+ }, value ?? (previous && touchedRef.current ? previous.value() : defaultValue));
69
+ if (previous)
70
+ store.expandValues(previous.expandedValues());
71
+ else if (defaultExpanded === "all")
72
+ store.expandAll();
73
+ else if (typeof defaultExpanded === "number")
74
+ store.expandAll(defaultExpanded);
75
+ else if (defaultExpanded)
76
+ store.expandValues(defaultExpanded);
77
+ holderRef.current = { config, store, items, value };
78
+ }
79
+ const holder = holderRef.current;
80
+ const store = holder.store;
81
+ if (!sameItems(holder.items, items)) {
82
+ store.setItems(items, value ?? (touchedRef.current ? store.value() : defaultValue));
83
+ holder.items = items;
84
+ holder.value = value;
85
+ }
86
+ if (value !== undefined && value !== holder.value) {
87
+ store.setValue(value);
88
+ holder.value = value;
89
+ }
90
+ const [version, bump] = useReducer((count) => count + 1, 0);
91
+ const [range, setRange] = useState(() => ({ start: 0, end: Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight) + OVERSCAN }));
92
+ const rangeRef = useRef(range);
93
+ rangeRef.current = range;
94
+ const [motion, setMotion] = useState(null);
95
+ const motionIdRef = useRef(0);
96
+ const [focusKey, setFocusKey] = useState(null);
97
+ const [editing, setEditing] = useState(null);
98
+ const editingRef = useRef(null);
99
+ editingRef.current = editing;
100
+ const editTextRef = useRef("");
101
+ const [saving, setSaving] = useState(false);
102
+ const savingRef = useRef(false);
103
+ const [busyKeys, setBusyKeys] = useState(() => new Set());
104
+ const [message, setMessage] = useState(null);
105
+ const [contextKey, setContextKey] = useState(undefined);
106
+ const panelRef = useRef(null);
107
+ const viewportRef = useRef(null);
108
+ const motionRef = useRef(null);
109
+ const hadFocusRef = useRef(false);
110
+ const restoringRef = useRef(false);
111
+ const pendingScrollRef = useRef(null);
112
+ const pendingFocusRef = useRef(null);
113
+ const typeaheadRef = useRef({ text: "", at: 0 });
114
+ const blocked = readOnly || disabled;
115
+ const noticeAnchor = (target) => {
116
+ const row = target.closest(".nx-tree__row");
117
+ return row?.querySelector(".nx-tree__check, .nx-tree__text") ?? row;
118
+ };
119
+ const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, viewportRef, readOnlyMessage, { anchor: noticeAnchor, typing: false, lock: "label" });
120
+ const lockedNotice = useReadOnlyNotice(!blocked, viewportRef, lockedMessage, { anchor: noticeAnchor, typing: false, lock: "label" });
121
+ // The row a note points at: when it leaves the page (scrolled out of the window), the note goes too.
122
+ const noteRowRef = useRef(null);
123
+ function showNote(notice, target) {
124
+ noteRowRef.current = target?.closest(".nx-tree__row") ?? null;
125
+ notice.show(target);
126
+ }
127
+ const rows = store.rows;
128
+ const total = rows.length;
129
+ // The value for a form, read again only when something could have changed it.
130
+ const formValues = useMemo(() => (name ? store.value() : []), [name, store, version, items, value]);
131
+ function rowElement(key) {
132
+ const node = key === null ? undefined : store.get(key);
133
+ return node ? (viewportRef.current?.querySelector(`[data-uid="${node.uid}"]`) ?? null) : null;
134
+ }
135
+ function nodeOfTarget(target) {
136
+ const row = target instanceof Element ? target.closest(".nx-tree__row[data-key]") : null;
137
+ return row?.dataset.key === undefined ? undefined : store.get(row.dataset.key);
138
+ }
139
+ function focusedNode() {
140
+ const node = focusKey === null ? undefined : store.get(focusKey);
141
+ return node && store.isVisible(node) ? node : undefined;
142
+ }
143
+ // Scrolling and focus
144
+ function syncRange(viewport, force = false) {
145
+ const top = Math.max(0, viewport.scrollTop - PAD);
146
+ const first = Math.floor(top / rowHeight);
147
+ const last = Math.ceil((top + viewport.clientHeight) / rowHeight);
148
+ const current = rangeRef.current;
149
+ const covered = first >= current.start && last <= current.end && current.end - current.start <= last - first + 4 * OVERSCAN;
150
+ if (covered && !force)
151
+ return;
152
+ const next = { start: Math.max(0, first - OVERSCAN), end: last + OVERSCAN };
153
+ if (next.start === current.start && next.end === current.end)
154
+ return;
155
+ rangeRef.current = next;
156
+ setRange(next);
157
+ }
158
+ function scrollToIndex(index) {
159
+ const viewport = viewportRef.current;
160
+ if (!viewport || index < 0)
161
+ return;
162
+ const top = PAD + index * rowHeight;
163
+ if (top - PAD < viewport.scrollTop)
164
+ viewport.scrollTop = top - PAD;
165
+ else if (top + rowHeight + PAD > viewport.scrollTop + viewport.clientHeight)
166
+ viewport.scrollTop = top + rowHeight + PAD - viewport.clientHeight;
167
+ syncRange(viewport);
168
+ }
169
+ /** Moves focus to a row: scrolls it into view after the next render, then focuses it. */
170
+ function focusNode(node) {
171
+ if (store.reveal(node))
172
+ setMotion(null);
173
+ setFocusKey(node.key);
174
+ pendingScrollRef.current = store.indexOf(node);
175
+ pendingFocusRef.current = node.key;
176
+ bump();
177
+ }
178
+ function moveTo(index) {
179
+ if (total === 0)
180
+ return;
181
+ const node = rows[Math.max(0, Math.min(index, total - 1))];
182
+ if (node)
183
+ focusNode(node);
184
+ }
185
+ useLayoutEffect(() => {
186
+ const viewport = viewportRef.current;
187
+ if (!viewport)
188
+ return;
189
+ const scrollIndex = pendingScrollRef.current;
190
+ if (scrollIndex !== null) {
191
+ pendingScrollRef.current = null;
192
+ scrollToIndex(scrollIndex);
193
+ }
194
+ const key = pendingFocusRef.current;
195
+ if (key !== null) {
196
+ const row = rowElement(key);
197
+ if (row) {
198
+ pendingFocusRef.current = null;
199
+ // An open editor in the row keeps focus.
200
+ if (!row.contains(document.activeElement))
201
+ row.focus({ preventScroll: true });
202
+ }
203
+ return;
204
+ }
205
+ // A focused row that scrolled out of the window, or was replaced, leaves focus nowhere: keep it in the tree.
206
+ const active = document.activeElement;
207
+ if (hadFocusRef.current && (!active || active === document.body)) {
208
+ restoringRef.current = true;
209
+ (rowElement(focusKey) ?? viewport).focus({ preventScroll: true });
210
+ restoringRef.current = false;
211
+ }
212
+ });
213
+ useLayoutEffect(() => {
214
+ const viewport = viewportRef.current;
215
+ if (!viewport)
216
+ return;
217
+ syncRange(viewport, true);
218
+ if (typeof ResizeObserver === "undefined")
219
+ return;
220
+ const observer = new ResizeObserver(() => syncRange(viewport));
221
+ observer.observe(viewport);
222
+ return () => observer.disconnect();
223
+ // eslint-disable-next-line react-hooks/exhaustive-deps
224
+ }, [rowHeight]);
225
+ // A note under a row that scrolled away would point at nothing.
226
+ useLayoutEffect(() => {
227
+ const row = noteRowRef.current;
228
+ if (!row || row.isConnected)
229
+ return;
230
+ noteRowRef.current = null;
231
+ readOnlyNotice.hide();
232
+ lockedNotice.hide();
233
+ });
234
+ // An open or close ends when its animation does, or at once when none runs (reduced motion).
235
+ useLayoutEffect(() => {
236
+ if (!motion)
237
+ return;
238
+ const group = motionRef.current;
239
+ if (!group) {
240
+ setMotion(null);
241
+ return;
242
+ }
243
+ return afterAnimation(group, motion.type === "open" ? "nx-tree-open" : "nx-tree-close", () => setMotion(null));
244
+ }, [motion]);
245
+ function onPanelFocus() {
246
+ hadFocusRef.current = true;
247
+ }
248
+ function onPanelBlur(event) {
249
+ const next = event.relatedTarget;
250
+ if (next instanceof Node && panelRef.current?.contains(next))
251
+ return;
252
+ if (next) {
253
+ hadFocusRef.current = false;
254
+ readOnlyNotice.hide();
255
+ lockedNotice.hide();
256
+ return;
257
+ }
258
+ // Focus went nowhere: a click on the page, or a focused row that left the DOM. The first is settled after this frame.
259
+ requestAnimationFrame(() => {
260
+ if (!panelRef.current?.contains(document.activeElement))
261
+ hadFocusRef.current = false;
262
+ });
263
+ }
264
+ function onViewportFocus(event) {
265
+ // A row focused any way (a click, Tab, a script) becomes the row keys move from.
266
+ const focused = nodeOfTarget(event.target);
267
+ if (focused && event.target === rowElement(focused.key)) {
268
+ setFocusKey(focused.key);
269
+ return;
270
+ }
271
+ if (event.target !== event.currentTarget || restoringRef.current)
272
+ return;
273
+ // Tab reached the tree while its focused row is out of view: bring the row back.
274
+ const node = focusedNode() ?? rows[0];
275
+ if (node)
276
+ focusNode(node);
277
+ }
278
+ // Changes and events
279
+ function emitValue() {
280
+ onValueChange?.(store.value(), store.valueItems());
281
+ }
282
+ function afterCheck() {
283
+ touchedRef.current = true;
284
+ // Controlled: the next render sets the tree back to `value`, unless the page passes the new one.
285
+ if (value !== undefined)
286
+ holder.value = undefined;
287
+ bump();
288
+ emitValue();
289
+ }
290
+ function emitItems() {
291
+ if (!onItemsChange)
292
+ return;
293
+ const list = store.items();
294
+ holder.items = list;
295
+ onItemsChange(list);
296
+ }
297
+ function valueSignature() {
298
+ return onValueChange ? store.value().join("") : "";
299
+ }
300
+ function emitValueIfChanged(before) {
301
+ if (onValueChange && valueSignature() !== before)
302
+ afterCheck();
303
+ }
304
+ function setBusy(key, busy) {
305
+ setBusyKeys(current => {
306
+ const next = new Set(current);
307
+ if (busy)
308
+ next.add(key);
309
+ else
310
+ next.delete(key);
311
+ return next;
312
+ });
313
+ }
314
+ function openMotion(change) {
315
+ return change.count > 0 ? { id: ++motionIdRef.current, type: "open", at: change.at, count: change.count } : null;
316
+ }
317
+ function closeMotion(change) {
318
+ const visible = change.before.slice(change.at, Math.min(change.at + change.count, rangeRef.current.end));
319
+ return visible.length > 0 && change.at >= rangeRef.current.start ? { id: ++motionIdRef.current, type: "close", at: change.at, rows: visible } : null;
320
+ }
321
+ function toggleCheck(node, target) {
322
+ if (!checkable || disabled || node.draft)
323
+ return;
324
+ if (readOnly) {
325
+ showNote(readOnlyNotice, target);
326
+ return;
327
+ }
328
+ if (node.locked) {
329
+ showNote(lockedNotice, target);
330
+ return;
331
+ }
332
+ if (store.toggle(node))
333
+ afterCheck();
334
+ }
335
+ function toggleAll() {
336
+ if (blocked)
337
+ return;
338
+ if (store.toggleAll())
339
+ afterCheck();
340
+ }
341
+ function toggleExpand(node) {
342
+ if (node.children.length === 0)
343
+ return;
344
+ const edit = editingRef.current;
345
+ if (edit && node.expanded && isInside(store.get(edit.key), node)) {
346
+ void runAfterEdit(() => toggleExpand(node));
347
+ return;
348
+ }
349
+ const change = node.expanded ? store.collapse(node) : store.expand(node);
350
+ setMotion(change ? (node.expanded ? openMotion(change) : closeMotion(change)) : null);
351
+ // Focus inside a collapsed item moves up to it.
352
+ const focused = focusKey === null ? undefined : store.get(focusKey);
353
+ if (focused && !store.isVisible(focused) && hadFocusRef.current)
354
+ focusNode(node);
355
+ bump();
356
+ }
357
+ function expandAll() {
358
+ store.expandAll();
359
+ setMotion(null);
360
+ bump();
361
+ }
362
+ function collapseAll() {
363
+ const focused = focusedNode();
364
+ store.collapseAll();
365
+ setMotion(null);
366
+ if (focused) {
367
+ let top = focused;
368
+ while (top.parent)
369
+ top = top.parent;
370
+ setFocusKey(top.key);
371
+ pendingScrollRef.current = store.indexOf(top);
372
+ }
373
+ bump();
374
+ }
375
+ // Adding, renaming, deleting
376
+ function allowed(action, node) {
377
+ if (blocked)
378
+ return false;
379
+ if ((action === "create" && !creatable) || (action === "update" && !editable) || (action === "delete" && !deletable))
380
+ return false;
381
+ if (node && (node.disabledPath || node.draft))
382
+ return false;
383
+ return canChange ? canChange(action, node ? node.item : null) : true;
384
+ }
385
+ /** Why an action on a row does nothing: a note for a locked item or a read-only tree. */
386
+ function explainRefusal(action, node) {
387
+ const target = node ? rowElement(node.key) : null;
388
+ const offered = (action === "create" && creatable) || (action === "update" && editable) || (action === "delete" && deletable);
389
+ if (!offered || disabled)
390
+ return;
391
+ if (readOnly)
392
+ showNote(readOnlyNotice, target);
393
+ else if (node?.disabledPath)
394
+ showNote(lockedNotice, target);
395
+ }
396
+ /** Saves an open editor first, so switching to another action never drops what was typed. */
397
+ async function runAfterEdit(action) {
398
+ if (editingRef.current && !(await commitEdit("switch")))
399
+ return;
400
+ action();
401
+ }
402
+ function openEditor(edit, text) {
403
+ editTextRef.current = text;
404
+ editingRef.current = edit;
405
+ setEditing(edit);
406
+ setMessage(null);
407
+ }
408
+ function closeEditor() {
409
+ editingRef.current = null;
410
+ setEditing(null);
411
+ }
412
+ function startCreate(parent) {
413
+ if (!allowed("create", parent)) {
414
+ explainRefusal("create", parent);
415
+ return;
416
+ }
417
+ void runAfterEdit(() => {
418
+ if (parent && store.get(parent.key) !== parent)
419
+ return;
420
+ if (parent && store.reveal(parent))
421
+ setMotion(null);
422
+ const wasOpen = parent ? parent.expanded : true;
423
+ const parentIndex = parent ? store.indexOf(parent) : -1;
424
+ const before = store.rows;
425
+ const draft = { [valueKey]: newTreeValue(), [parentKey]: parent ? parent.value : null, [labelKey]: "" };
426
+ const node = store.insert(parent, draft, { draft: true });
427
+ const index = store.indexOf(node);
428
+ setMotion(openMotion(wasOpen ? { at: index, count: 1 } : { at: parentIndex + 1, count: store.rows.length - before.length }));
429
+ openEditor({ key: node.key, mode: "create" }, "");
430
+ setFocusKey(node.key);
431
+ pendingScrollRef.current = index;
432
+ bump();
433
+ });
434
+ }
435
+ function startRename(node) {
436
+ if (!allowed("update", node)) {
437
+ explainRefusal("update", node);
438
+ return;
439
+ }
440
+ void runAfterEdit(() => {
441
+ if (store.get(node.key) !== node)
442
+ return;
443
+ if (store.reveal(node))
444
+ setMotion(null);
445
+ openEditor({ key: node.key, mode: "update" }, node.label);
446
+ setFocusKey(node.key);
447
+ pendingScrollRef.current = store.indexOf(node);
448
+ bump();
449
+ });
450
+ }
451
+ function removeNode(node) {
452
+ const index = store.indexOf(node);
453
+ const before = store.rows;
454
+ store.remove(node);
455
+ setMotion(index >= 0 ? closeMotion({ at: index, count: before.length - store.rows.length, before }) : null);
456
+ return index;
457
+ }
458
+ function cancelEdit() {
459
+ const edit = editingRef.current;
460
+ if (!edit || savingRef.current)
461
+ return;
462
+ const node = store.get(edit.key);
463
+ closeEditor();
464
+ setMessage(null);
465
+ if (!node)
466
+ return;
467
+ if (edit.mode === "create" && node.draft) {
468
+ const index = removeNode(node);
469
+ const next = node.parent ?? store.rows[Math.max(0, Math.min(index, store.rows.length) - 1)];
470
+ if (next)
471
+ focusNode(next);
472
+ }
473
+ else {
474
+ focusNode(node);
475
+ }
476
+ bump();
477
+ }
478
+ /** Saves the open editor. Resolves with whether it closed, so a following action may run. */
479
+ async function commitEdit(via) {
480
+ const edit = editingRef.current;
481
+ if (!edit)
482
+ return true;
483
+ if (savingRef.current)
484
+ return false;
485
+ const node = store.get(edit.key);
486
+ if (!node) {
487
+ closeEditor();
488
+ return true;
489
+ }
490
+ const text = editTextRef.current.trim();
491
+ if (edit.mode === "update" && text === node.label) {
492
+ closeEditor();
493
+ if (via === "enter")
494
+ focusNode(node);
495
+ return true;
496
+ }
497
+ if (!text) {
498
+ if (via !== "enter") {
499
+ cancelEdit();
500
+ return true;
501
+ }
502
+ setMessage("Enter a name.");
503
+ return false;
504
+ }
505
+ savingRef.current = true;
506
+ setSaving(true);
507
+ setBusy(node.key, true);
508
+ const key = node.key;
509
+ const valueBefore = valueSignature();
510
+ try {
511
+ let item;
512
+ if (edit.mode === "create") {
513
+ const parent = node.parent;
514
+ const draft = { ...node.item, [labelKey]: text };
515
+ const result = await onCreate?.({ parent: parent ? parent.item : null, parentValue: parent ? parent.value : null, label: text, item: draft });
516
+ item = isItem(result) ? result : draft;
517
+ }
518
+ else {
519
+ const renamed = { ...node.item, [labelKey]: text };
520
+ const result = await onUpdate?.({ item: node.item, value: node.value, label: text });
521
+ item = isItem(result) ? result : renamed;
522
+ }
523
+ // New items may have come meanwhile; the node is gone with them.
524
+ if (store.get(key) === node)
525
+ store.update(node, item);
526
+ if (editingRef.current === edit)
527
+ closeEditor();
528
+ setMessage(null);
529
+ if (store.get(node.key) === node && (via === "enter" || hadFocusRef.current))
530
+ focusNode(node);
531
+ emitItems();
532
+ emitValueIfChanged(valueBefore);
533
+ bump();
534
+ return true;
535
+ }
536
+ catch (reason) {
537
+ setMessage(`Could not save "${text}": ${errorText(reason)}`);
538
+ pendingFocusRef.current = null;
539
+ requestAnimationFrame(() => panelRef.current?.querySelector(".nx-tree__input")?.focus());
540
+ return false;
541
+ }
542
+ finally {
543
+ savingRef.current = false;
544
+ setSaving(false);
545
+ setBusy(key, false);
546
+ }
547
+ }
548
+ async function requestDelete(node) {
549
+ if (!allowed("delete", node)) {
550
+ explainRefusal("delete", node);
551
+ return false;
552
+ }
553
+ if (editingRef.current && !(await commitEdit("switch")))
554
+ return false;
555
+ if (store.get(node.key) !== node)
556
+ return false;
557
+ const subtree = store.subtree(node);
558
+ if (subtree.some(item => item.disabledPath)) {
559
+ setMessage(`"${node.label}" holds items you cannot change, so it cannot be deleted.`);
560
+ return false;
561
+ }
562
+ const name = node.label || "this item";
563
+ if (confirmDelete) {
564
+ const under = subtree.length - 1;
565
+ const confirmed = await messageBox.confirm({
566
+ title: `Delete "${name}"?`,
567
+ message: under > 0 ? `The ${under.toLocaleString()} ${under === 1 ? "item" : "items"} under it will be deleted too.` : "This cannot be undone.",
568
+ tone: "danger",
569
+ confirmLabel: "Delete",
570
+ });
571
+ if (!confirmed) {
572
+ if (store.get(node.key) === node)
573
+ focusNode(node);
574
+ return false;
575
+ }
576
+ }
577
+ if (store.get(node.key) !== node)
578
+ return false;
579
+ const key = node.key;
580
+ setBusy(key, true);
581
+ try {
582
+ await onDelete?.({ item: node.item, value: node.value, items: subtree.filter(item => !item.draft).map(item => item.item) });
583
+ if (store.get(key) !== node)
584
+ return true;
585
+ const valueBefore = valueSignature();
586
+ const index = removeNode(node);
587
+ const next = store.rows[Math.max(0, Math.min(index, store.rows.length - 1))];
588
+ if (next)
589
+ focusNode(next);
590
+ setMessage(null);
591
+ emitItems();
592
+ emitValueIfChanged(valueBefore);
593
+ bump();
594
+ return true;
595
+ }
596
+ catch (reason) {
597
+ setMessage(`Could not delete "${name}": ${errorText(reason)}`);
598
+ return false;
599
+ }
600
+ finally {
601
+ setBusy(key, false);
602
+ }
603
+ }
604
+ // Pointer and keys
605
+ function onViewportPointerDown(event) {
606
+ // The context menu opens on the row that was pressed, or at the top level on empty space.
607
+ const node = nodeOfTarget(event.target);
608
+ setContextKey(node ? node.key : null);
609
+ }
610
+ function onViewportClick(event) {
611
+ const target = event.target;
612
+ if (target.closest(".nx-tree__edit, .nx-tree__actions, .nx-tree__motion--close"))
613
+ return;
614
+ const node = nodeOfTarget(target);
615
+ if (!node)
616
+ return;
617
+ setFocusKey(node.key);
618
+ if (disabled)
619
+ return;
620
+ if (target.closest(".nx-tree__toggle")) {
621
+ toggleExpand(node);
622
+ return;
623
+ }
624
+ if (node.draft)
625
+ return;
626
+ if (checkable) {
627
+ toggleCheck(node, target);
628
+ }
629
+ else if (onItemClick) {
630
+ if (node.disabledPath)
631
+ showNote(lockedNotice, target);
632
+ else
633
+ onItemClick(node.item);
634
+ }
635
+ else if (node.children.length > 0) {
636
+ toggleExpand(node);
637
+ }
638
+ }
639
+ function onViewportKeyDown(event) {
640
+ if (event.defaultPrevented || event.target.closest(".nx-tree__edit"))
641
+ return;
642
+ setContextKey(undefined);
643
+ if (event.key === "Escape" && noteRowRef.current) {
644
+ noteRowRef.current = null;
645
+ readOnlyNotice.hide();
646
+ lockedNotice.hide();
647
+ event.preventDefault();
648
+ return;
649
+ }
650
+ if (disabled || total === 0)
651
+ return;
652
+ const node = focusedNode() ?? rows[0];
653
+ const index = store.indexOf(node);
654
+ const target = rowElement(node.key) ?? event.currentTarget;
655
+ const page = Math.max(1, Math.floor((viewportRef.current?.clientHeight ?? rowHeight) / rowHeight) - 1);
656
+ const { key, ctrlKey, metaKey, altKey } = event;
657
+ let handled = true;
658
+ if (key === "ArrowDown")
659
+ moveTo(index + 1);
660
+ else if (key === "ArrowUp")
661
+ moveTo(index - 1);
662
+ else if (key === "Home")
663
+ moveTo(0);
664
+ else if (key === "End")
665
+ moveTo(total - 1);
666
+ else if (key === "PageDown")
667
+ moveTo(index + page);
668
+ else if (key === "PageUp")
669
+ moveTo(index - page);
670
+ else if (key === "ArrowRight") {
671
+ if (node.children.length === 0)
672
+ handled = false;
673
+ else if (!node.expanded)
674
+ toggleExpand(node);
675
+ else
676
+ moveTo(index + 1);
677
+ }
678
+ else if (key === "ArrowLeft") {
679
+ if (node.expanded)
680
+ toggleExpand(node);
681
+ else if (node.parent)
682
+ focusNode(node.parent);
683
+ else
684
+ handled = false;
685
+ }
686
+ else if (key === " " && !ctrlKey && !metaKey) {
687
+ if (checkable)
688
+ toggleCheck(node, target);
689
+ }
690
+ else if (key === "Enter") {
691
+ if (onItemClick && !checkable && !node.disabledPath)
692
+ onItemClick(node.item);
693
+ else if (node.children.length > 0)
694
+ toggleExpand(node);
695
+ else if (checkable)
696
+ toggleCheck(node, target);
697
+ }
698
+ else if (key === "*") {
699
+ const siblings = node.parent ? node.parent.children : store.roots;
700
+ store.expandValues(siblings.filter(sibling => sibling.children.length > 0).map(sibling => sibling.value));
701
+ setMotion(null);
702
+ bump();
703
+ }
704
+ else if (key === "F2")
705
+ startRename(node);
706
+ else if (key === "Delete")
707
+ void requestDelete(node);
708
+ else if (key === "Insert")
709
+ startCreate(node);
710
+ else if (key.length === 1 && !ctrlKey && !metaKey && !altKey && key !== " ") {
711
+ const now = performance.now();
712
+ const typed = typeaheadRef.current;
713
+ typed.text = now - typed.at < TYPEAHEAD_DELAY ? typed.text + key.toLowerCase() : key.toLowerCase();
714
+ typed.at = now;
715
+ // The same letter again moves to the next row starting with it; a longer text keeps the row while it still matches.
716
+ const repeated = [...typed.text].every(letter => letter === typed.text[0]);
717
+ const search = repeated ? typed.text.slice(0, 1) : typed.text;
718
+ const from = repeated ? index + 1 : index;
719
+ for (let step = 0; step < total; step++) {
720
+ const candidate = rows[(from + step) % total];
721
+ if (candidate.label.toLowerCase().startsWith(search)) {
722
+ focusNode(candidate);
723
+ break;
724
+ }
725
+ }
726
+ }
727
+ else
728
+ handled = false;
729
+ if (handled)
730
+ event.preventDefault();
731
+ }
732
+ // The context menu
733
+ const crud = !blocked && (creatable || editable || deletable);
734
+ const contextNode = contextKey === undefined ? focusedNode() : contextKey === null ? undefined : store.get(contextKey);
735
+ const contextItems = [];
736
+ if (crud) {
737
+ if (creatable) {
738
+ contextItems.push({
739
+ value: "create",
740
+ label: contextNode ? "Add item under it" : "Add item",
741
+ icon: _jsx(InlineIcon, { icon: iconPlus }),
742
+ shortcut: contextNode ? "Insert" : undefined,
743
+ disabled: !allowed("create", contextNode ?? null),
744
+ });
745
+ }
746
+ if (editable && contextNode) {
747
+ contextItems.push({ value: "update", label: "Rename", icon: _jsx(InlineIcon, { icon: iconPencil }), shortcut: "F2", disabled: !allowed("update", contextNode) });
748
+ }
749
+ if (deletable && contextNode) {
750
+ if (contextItems.length > 0)
751
+ contextItems.push({ separator: true });
752
+ contextItems.push({ value: "delete", label: "Delete", icon: _jsx(InlineIcon, { icon: iconTrash }), shortcut: "Delete", tone: "danger", disabled: !allowed("delete", contextNode) });
753
+ }
754
+ }
755
+ function onContextSelect(action) {
756
+ if (action === "create")
757
+ startCreate(contextNode ?? null);
758
+ else if (action === "update" && contextNode)
759
+ startRename(contextNode);
760
+ else if (action === "delete" && contextNode)
761
+ void requestDelete(contextNode);
762
+ }
763
+ useImperativeHandle(ref, () => ({
764
+ expandAll: levels => {
765
+ store.expandAll(levels);
766
+ setMotion(null);
767
+ bump();
768
+ },
769
+ collapseAll,
770
+ expand: expandValue => {
771
+ const node = store.get(expandValue);
772
+ if (!node)
773
+ return;
774
+ store.reveal(node);
775
+ store.expand(node);
776
+ setMotion(null);
777
+ bump();
778
+ },
779
+ collapse: collapseValue => {
780
+ const node = store.get(collapseValue);
781
+ if (node && store.collapse(node)) {
782
+ setMotion(null);
783
+ bump();
784
+ }
785
+ },
786
+ reveal: (revealValue, focus = true) => {
787
+ const node = store.get(revealValue);
788
+ if (!node)
789
+ return;
790
+ if (focus) {
791
+ focusNode(node);
792
+ return;
793
+ }
794
+ if (store.reveal(node))
795
+ setMotion(null);
796
+ pendingScrollRef.current = store.indexOf(node);
797
+ bump();
798
+ },
799
+ getValue: () => store.value(),
800
+ setValue: values => {
801
+ if (!store.setValue(values))
802
+ return;
803
+ touchedRef.current = true;
804
+ bump();
805
+ },
806
+ checkAll: checked => {
807
+ if (store.checkAll(checked))
808
+ afterCheck();
809
+ },
810
+ getItems: () => store.items(),
811
+ startCreate: parentValue => startCreate(parentValue === null ? null : (store.get(parentValue) ?? null)),
812
+ startRename: renameValue => {
813
+ const node = store.get(renameValue);
814
+ if (node)
815
+ startRename(node);
816
+ },
817
+ remove: removeValue => {
818
+ const node = store.get(removeValue);
819
+ return node ? requestDelete(node) : Promise.resolve(false);
820
+ },
821
+ focus: () => {
822
+ const node = focusedNode() ?? rows[0];
823
+ if (node)
824
+ focusNode(node);
825
+ },
826
+ }));
827
+ // Rendering
828
+ const showCounts = checkable && cascade && counts;
829
+ const tabbable = focusedNode() ?? rows[0];
830
+ const rendered = new Set();
831
+ function renderRow(node, ghost = false) {
832
+ if (!ghost)
833
+ rendered.add(node.key);
834
+ const hasChildren = node.children.length > 0;
835
+ const state = checkable ? store.state(node) : null;
836
+ const edit = !ghost && editing?.key === node.key ? editing : null;
837
+ const busy = busyKeys.has(node.key);
838
+ const locked = checkable ? node.locked : node.disabledPath;
839
+ const labelElementId = `${treeId}-l${node.uid}`;
840
+ const description = read(node.item, descriptionKey);
841
+ const hasDescription = typeof description === "string" ? description !== "" : typeof description === "number";
842
+ const descriptionElementId = hasDescription ? `${treeId}-d${node.uid}` : undefined;
843
+ const icon = iconOf(read(node.item, iconKey));
844
+ const actions = [];
845
+ if (!ghost && !edit && !busy && !node.draft) {
846
+ if (allowed("create", node)) {
847
+ actions.push(_jsx(Button, { variant: "ghost", size: "sm", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: iconPlus }), "aria-label": `Add an item under ${node.label}`, title: "Add item (Insert)", tabIndex: -1, onMouseDown: keepEditorFocus, onClick: () => startCreate(node) }, "create"));
848
+ }
849
+ if (allowed("update", node)) {
850
+ actions.push(_jsx(Button, { variant: "ghost", size: "sm", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: iconPencil }), "aria-label": `Rename ${node.label}`, title: "Rename (F2)", tabIndex: -1, onMouseDown: keepEditorFocus, onClick: () => startRename(node) }, "update"));
851
+ }
852
+ if (allowed("delete", node)) {
853
+ actions.push(_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Delete ${node.label}`, title: "Delete (Delete)", tabIndex: -1, onMouseDown: keepEditorFocus, onClick: () => void requestDelete(node) }, "delete"));
854
+ }
855
+ }
856
+ return (_jsxs("div", { role: ghost ? undefined : "treeitem", className: cx("nx-tree__row", locked && "nx-tree__row--locked", edit && "nx-tree__row--editing"), style: { "--nx-tree-depth": node.depth }, "data-key": ghost ? undefined : node.key, "data-uid": ghost ? undefined : node.uid, "data-readonly": locked || undefined, tabIndex: ghost ? undefined : !disabled && tabbable === node ? 0 : -1, "aria-level": ghost ? undefined : node.depth + 1, "aria-setsize": ghost ? undefined : (node.parent ? node.parent.children.length : store.roots.length), "aria-posinset": ghost ? undefined : node.position + 1, "aria-expanded": ghost || !hasChildren ? undefined : node.expanded, "aria-checked": ghost || !state ? undefined : state === "mixed" ? "mixed" : state === "checked", "aria-disabled": ghost ? undefined : locked || undefined, "aria-busy": busy || undefined, "aria-labelledby": ghost || edit ? undefined : labelElementId, "aria-label": edit ? (edit.mode === "create" ? "New item" : `Renaming ${node.label}`) : undefined, "aria-describedby": ghost || edit ? undefined : descriptionElementId, children: [_jsx("span", { className: "nx-tree__toggle", "aria-hidden": "true", children: hasChildren && _jsx(InlineIcon, { icon: iconChevronRight }) }), checkable && (_jsx("span", { className: cx("nx-tree__check nx-choice", `nx-choice--${size}`), "aria-hidden": "true", children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", tabIndex: -1, checked: state === "checked", disabled: disabled, readOnly: true, ref: input => {
857
+ if (input)
858
+ input.indeterminate = state === "mixed";
859
+ } }) })), icon && (_jsx("span", { className: "nx-tree__icon", "aria-hidden": "true", children: icon })), edit ? (_jsx(TreeEditor, { initial: editTextRef.current, select: edit.mode === "update", maxLength: maxLabelLength, saving: saving, label: edit.mode === "create" ? "Name of the new item" : `New name for ${node.label}`, invalid: message !== null, describedBy: message !== null ? messageId : undefined, onText: text => (editTextRef.current = text), onCommit: via => void commitEdit(via), onCancel: cancelEdit }, node.uid)) : (_jsxs("span", { className: "nx-tree__text", children: [_jsx("span", { id: ghost ? undefined : labelElementId, className: "nx-tree__label", title: node.label, children: node.label || "(no name)" }), hasDescription && (_jsx("span", { id: ghost ? undefined : descriptionElementId, className: "nx-tree__description", children: String(description) }))] })), showCounts && hasChildren && !edit && (_jsxs("span", { className: "nx-tree__count", "aria-hidden": "true", children: [node.checkedLeaves.toLocaleString(), "/", node.leaves.toLocaleString()] })), busy && !edit && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin nx-tree__busy", "aria-hidden": "true" }), locked && !edit && (_jsx("span", { className: "nx-tree__lock", title: lockedMessage, "aria-hidden": "true", children: _jsx(InlineIcon, { icon: iconLock }) })), actions.length > 0 && _jsx("span", { className: "nx-tree__actions", children: actions })] }, node.uid));
860
+ }
861
+ const end = Math.min(range.end, total);
862
+ const start = Math.min(range.start, end);
863
+ let body;
864
+ let bottom;
865
+ const openValid = motion?.type === "open" && motion.at >= start && motion.at < end && motion.at + motion.count <= total;
866
+ const closeValid = motion?.type === "close" && motion.at >= start && motion.at <= end;
867
+ if (motion?.type === "open" && openValid) {
868
+ const groupEnd = Math.min(motion.at + motion.count, end);
869
+ const afterStart = motion.at + motion.count;
870
+ const afterEnd = Math.min(total, afterStart + (end - motion.at));
871
+ body = [
872
+ ...rows.slice(start, motion.at).map(node => renderRow(node)),
873
+ _jsx("div", { ref: motionRef, role: "none", className: "nx-tree__motion nx-tree__motion--open", style: { "--nx-tree-motion-height": `${(groupEnd - motion.at) * rowHeight}px` }, children: rows.slice(motion.at, groupEnd).map(node => renderRow(node)) }, `motion-${motion.id}`),
874
+ ...rows.slice(afterStart, afterEnd).map(node => renderRow(node)),
875
+ ];
876
+ bottom = (total - afterEnd) * rowHeight;
877
+ }
878
+ else if (motion?.type === "close" && closeValid) {
879
+ body = [
880
+ ...rows.slice(start, motion.at).map(node => renderRow(node)),
881
+ _jsx("div", { ref: motionRef, role: "none", "aria-hidden": "true", inert: true, className: "nx-tree__motion nx-tree__motion--close", style: { "--nx-tree-motion-height": `${motion.rows.length * rowHeight}px` }, children: motion.rows.map(node => renderRow(node, true)) }, `motion-${motion.id}`),
882
+ ...rows.slice(motion.at, end).map(node => renderRow(node)),
883
+ ];
884
+ bottom = (total - end) * rowHeight;
885
+ }
886
+ else {
887
+ body = rows.slice(start, end).map(node => renderRow(node));
888
+ bottom = (total - end) * rowHeight;
889
+ }
890
+ const allState = checkable ? store.allState() : null;
891
+ const tally = store.counts();
892
+ const hasParents = store.roots.some(root => root.children.length > 0);
893
+ const hasLabel = hasContent(label);
894
+ const shownMessage = message ?? (hasContent(error) ? error : null);
895
+ const empty = total === 0;
896
+ return (_jsxs("div", { className: cx("nx-field nx-field--standard", `nx-field--${size}`, "nx-tree-field", className), style: style, "data-invalid": shownMessage ? true : undefined, "data-readonly": readOnly || undefined, "data-disabled": disabled || undefined, children: [hasLabel && (_jsxs("span", { id: labelId, className: "nx-field__label", children: [label, readOnlyNotice.lock] })), _jsxs("div", { ref: panelRef, className: cx("nx-tree", `nx-tree--${size}`), onFocus: onPanelFocus, onBlur: onPanelBlur, children: [toolbar && (_jsxs("div", { className: "nx-tree__toolbar", children: [checkable && (_jsx(CheckBox, { size: "sm", className: "nx-tree__all", label: "Select all", checked: allState === "checked", indeterminate: allState === "mixed", disabled: disabled || allState === null || empty, readOnly: readOnly, readOnlyMessage: readOnlyMessage, onValueChange: toggleAll })), _jsxs("span", { className: "nx-tree__summary", "aria-live": "polite", children: [loading && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin", "aria-hidden": "true" }), loading && empty
897
+ ? "Loading…"
898
+ : checkable
899
+ ? `${tally.checked.toLocaleString()} of ${tally.total.toLocaleString()} selected`
900
+ : `${store.size.toLocaleString()} ${store.size === 1 ? "item" : "items"}`, !hasLabel && readOnlyNotice.lock] }), _jsxs("span", { className: "nx-tree__tools", children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconChevronsDown }), "aria-label": "Expand all", title: "Expand all", disabled: !hasParents, onClick: expandAll }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconChevronsUp }), "aria-label": "Collapse all", title: "Collapse all", disabled: !hasParents, onClick: collapseAll }), creatable && !blocked && (!canChange || canChange("create", null)) && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconPlus }), onMouseDown: keepEditorFocus, onClick: () => startCreate(null), children: "Add" }))] })] })), _jsx(ContextMenu, { items: contextItems, onSelect: onContextSelect, disabled: contextItems.length === 0, "aria-label": "Item actions", children: _jsx("div", { ref: viewportRef, className: "nx-tree__viewport", style: { height, maxHeight: height === undefined ? maxHeight : undefined }, tabIndex: !disabled && !empty && !(tabbable && rendered.has(tabbable.key)) ? 0 : -1, onScroll: event => syncRange(event.currentTarget), onFocus: onViewportFocus, onPointerDown: onViewportPointerDown, onClick: onViewportClick, onKeyDown: onViewportKeyDown, children: empty ? (loading ? (_jsx("div", { className: "nx-tree__list", "aria-hidden": "true", children: SKELETON_WIDTHS.map((width, index) => (_jsxs("div", { className: "nx-tree__row nx-tree__row--skeleton", style: { "--nx-tree-depth": index % 3 === 0 ? 0 : 1 }, children: [_jsx("span", { className: "nx-tree__toggle" }), _jsx("span", { className: "nx-tree__skeleton", style: { width } })] }, index))) })) : (_jsx("p", { className: "nx-tree__empty", children: emptyMessage }))) : (_jsx("div", { role: "tree", className: "nx-tree__list", "aria-labelledby": hasLabel ? labelId : undefined, "aria-label": hasLabel ? undefined : (ariaLabel ?? "Tree"), "aria-describedby": shownMessage || hasContent(hint) ? messageId : undefined, "aria-disabled": disabled || undefined, "aria-readonly": readOnly || undefined, "aria-busy": loading || undefined, style: { paddingTop: PAD + start * rowHeight, paddingBottom: PAD + bottom }, children: body })) }) })] }), (shownMessage || hasContent(hint)) && (_jsx("div", { className: "nx-field__footer", children: _jsxs("p", { id: messageId, className: cx("nx-field__message", shownMessage && "nx-field__message--error"), role: message ? "alert" : undefined, children: [shownMessage && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: shownMessage ?? hint })] }) })), name && formValues.map(item => _jsx("input", { type: "hidden", name: name, form: form, disabled: disabled, value: String(item) }, String(item))), readOnlyNotice.notice, lockedNotice.notice] }));
901
+ }
902
+ /** The name box of a row being added or renamed. Enter saves, Esc cancels, and leaving it saves. */
903
+ function TreeEditor({ initial, select, maxLength, saving, label, invalid, describedBy, onText, onCommit, onCancel }) {
904
+ const inputRef = useRef(null);
905
+ useLayoutEffect(() => {
906
+ const input = inputRef.current;
907
+ if (!input)
908
+ return;
909
+ input.focus({ preventScroll: true });
910
+ if (select)
911
+ input.select();
912
+ // eslint-disable-next-line react-hooks/exhaustive-deps
913
+ }, []);
914
+ return (_jsxs("span", { className: "nx-tree__edit", onContextMenu: event => event.stopPropagation(), children: [_jsx("input", { ref: inputRef, className: "nx-tree__input", defaultValue: initial, maxLength: maxLength, readOnly: saving, "aria-label": label, "aria-invalid": invalid || undefined, "aria-describedby": describedBy, spellCheck: true, onChange: event => onText(event.currentTarget.value), onKeyDown: event => {
915
+ event.stopPropagation();
916
+ if (event.key === "Enter") {
917
+ event.preventDefault();
918
+ onCommit("enter");
919
+ }
920
+ else if (event.key === "Escape") {
921
+ event.preventDefault();
922
+ onCancel();
923
+ }
924
+ }, onBlur: event => {
925
+ // Leaving the box saves, but not when the row left the page (scrolled away) or the window lost focus.
926
+ if (event.currentTarget.isConnected && document.hasFocus())
927
+ onCommit("blur");
928
+ } }), _jsx(Button, { variant: "ghost", size: "sm", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: saving ? iconLoader2 : iconCheck, className: saving ? "nx-spin" : undefined }), "aria-label": "Save", title: "Save (Enter)", tabIndex: -1, disabled: saving, onMouseDown: keepEditorFocus, onClick: () => onCommit("enter") }), _jsx(Button, { variant: "ghost", size: "sm", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: iconX }), "aria-label": "Cancel", title: "Cancel (Esc)", tabIndex: -1, disabled: saving, onMouseDown: keepEditorFocus, onClick: onCancel })] }));
929
+ }
930
+ /** A press on a row's button keeps focus where it is, so an open editor is not saved by the press itself. */
931
+ function keepEditorFocus(event) {
932
+ event.preventDefault();
933
+ }
934
+ function read(item, key) {
935
+ return item[key];
936
+ }
937
+ function iconOf(icon) {
938
+ if (typeof icon === "string" && icon)
939
+ return _jsx(Icon, { name: icon });
940
+ return isValidElement(icon) ? icon : null;
941
+ }
942
+ function isInside(node, ancestor) {
943
+ for (let parent = node?.parent; parent; parent = parent.parent)
944
+ if (parent === ancestor)
945
+ return true;
946
+ return false;
947
+ }