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.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- 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
|
+
}
|