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,63 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from "react";
|
|
2
|
+
/** A color in the editor's palette: the value stored in the HTML, and a lighter one shown in dark themes. */
|
|
3
|
+
export interface RichTextColor {
|
|
4
|
+
name: string;
|
|
5
|
+
/** The stored color, for light pages, emails, and print. `null` removes the color. */
|
|
6
|
+
value: string | null;
|
|
7
|
+
/** How text in this color shows in a dark theme. */
|
|
8
|
+
dark?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Text colors: dark enough for 4.5:1 on white, with a lighter twin for dark themes (Nexus.RichText.css). */
|
|
11
|
+
export declare const RICH_TEXT_COLORS: readonly RichTextColor[];
|
|
12
|
+
/** Highlights: light fills that keep dark text, in every theme. */
|
|
13
|
+
export declare const RICH_TEXT_HIGHLIGHTS: readonly RichTextColor[];
|
|
14
|
+
export interface ToolPanelHandle {
|
|
15
|
+
/** Opens the panel, as a click on its button does. */
|
|
16
|
+
open: () => void;
|
|
17
|
+
close: () => void;
|
|
18
|
+
}
|
|
19
|
+
interface ToolPanelProps {
|
|
20
|
+
label: string;
|
|
21
|
+
icon: ReactNode;
|
|
22
|
+
/** Shortcut for the button's tooltip, e.g. "Ctrl+K". */
|
|
23
|
+
shortcut?: string;
|
|
24
|
+
tabIndex: number;
|
|
25
|
+
/** Called before the panel opens, e.g. to read the selection it acts on. */
|
|
26
|
+
onOpen?: () => void;
|
|
27
|
+
/** The panel's content, rendered only while it is open. */
|
|
28
|
+
children: (close: () => void) => ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
ref?: Ref<ToolPanelHandle>;
|
|
31
|
+
/** `aria-pressed` of the button, e.g. while the caret is in a link. */
|
|
32
|
+
"data-state"?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A toolbar button that opens a panel under it in a native popover. The button keeps the editor's focus and
|
|
36
|
+
* selection on press; the panel takes focus when it opens, and Esc or a press outside closes it and gives focus back.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ToolPanel({ label, icon, shortcut, tabIndex, onOpen, children, className, ref, ...rest }: ToolPanelProps): import("react").JSX.Element;
|
|
39
|
+
interface ColorPanelProps {
|
|
40
|
+
/** The colors at the selection, as `#rrggbb`, or `null` for none. */
|
|
41
|
+
color: string | null;
|
|
42
|
+
background: string | null;
|
|
43
|
+
onPick: (kind: "color" | "background", value: string | null) => void;
|
|
44
|
+
}
|
|
45
|
+
/** Text colors and highlights, each a grid of swatches: arrow keys move, Enter or a click picks. */
|
|
46
|
+
export declare function ColorPanel({ color, background, onPick }: ColorPanelProps): import("react").JSX.Element;
|
|
47
|
+
interface LinkPanelProps {
|
|
48
|
+
/** The address of the link at the selection, when there is one. */
|
|
49
|
+
href: string | null;
|
|
50
|
+
/** Asks for the text too: the selection is empty and not in a link. */
|
|
51
|
+
askText: boolean;
|
|
52
|
+
onApply: (url: string, text: string) => void;
|
|
53
|
+
onRemove: () => void;
|
|
54
|
+
onCancel: () => void;
|
|
55
|
+
}
|
|
56
|
+
/** The link box: the address (and the text, when nothing is selected), with Apply, Remove, and Cancel. */
|
|
57
|
+
export declare function LinkPanel({ href, askText, onApply, onRemove, onCancel }: LinkPanelProps): import("react").JSX.Element;
|
|
58
|
+
/**
|
|
59
|
+
* A typed address as a link: "nexus.com" gets https://, an email gets mailto:, a phone number gets tel:. `null` when it
|
|
60
|
+
* is empty or not safe (`javascript:` and the like).
|
|
61
|
+
*/
|
|
62
|
+
export declare function normalizeLink(input: string): string | null;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconLinkOff, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { safeUrl } from "../../Helpers/MarkdownHelpers.js";
|
|
7
|
+
import { followAnchor, isPopoverOpen, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
|
|
8
|
+
import { Button } from "../Documents/Button.js";
|
|
9
|
+
import { TextBox } from "./TextBox.js";
|
|
10
|
+
/** Text colors: dark enough for 4.5:1 on white, with a lighter twin for dark themes (Nexus.RichText.css). */
|
|
11
|
+
export const RICH_TEXT_COLORS = [
|
|
12
|
+
{ name: "Default", value: null },
|
|
13
|
+
{ name: "Gray", value: "#4b5563", dark: "#9ca3af" },
|
|
14
|
+
{ name: "Red", value: "#b91c1c", dark: "#f87171" },
|
|
15
|
+
{ name: "Orange", value: "#9a3412", dark: "#fb923c" },
|
|
16
|
+
{ name: "Brown", value: "#92400e", dark: "#fbbf24" },
|
|
17
|
+
{ name: "Green", value: "#166534", dark: "#4ade80" },
|
|
18
|
+
{ name: "Teal", value: "#0f766e", dark: "#2dd4bf" },
|
|
19
|
+
{ name: "Blue", value: "#1d4ed8", dark: "#60a5fa" },
|
|
20
|
+
{ name: "Purple", value: "#7e22ce", dark: "#c084fc" },
|
|
21
|
+
{ name: "Pink", value: "#be185d", dark: "#f472b6" },
|
|
22
|
+
];
|
|
23
|
+
/** Highlights: light fills that keep dark text, in every theme. */
|
|
24
|
+
export const RICH_TEXT_HIGHLIGHTS = [
|
|
25
|
+
{ name: "None", value: null },
|
|
26
|
+
{ name: "Yellow", value: "#fef08a" },
|
|
27
|
+
{ name: "Green", value: "#bbf7d0" },
|
|
28
|
+
{ name: "Blue", value: "#bfdbfe" },
|
|
29
|
+
{ name: "Purple", value: "#e9d5ff" },
|
|
30
|
+
{ name: "Pink", value: "#fbcfe8" },
|
|
31
|
+
{ name: "Orange", value: "#fed7aa" },
|
|
32
|
+
{ name: "Gray", value: "#e5e7eb" },
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* A toolbar button that opens a panel under it in a native popover. The button keeps the editor's focus and
|
|
36
|
+
* selection on press; the panel takes focus when it opens, and Esc or a press outside closes it and gives focus back.
|
|
37
|
+
*/
|
|
38
|
+
export function ToolPanel({ label, icon, shortcut, tabIndex, onOpen, children, className, ref, ...rest }) {
|
|
39
|
+
const id = useId();
|
|
40
|
+
const [open, setOpen] = useState(false);
|
|
41
|
+
const buttonRef = useRef(null);
|
|
42
|
+
const panelRef = useRef(null);
|
|
43
|
+
// A press on the button while the panel is open closes it (light dismiss); the click that follows must not reopen it.
|
|
44
|
+
const dismissingRef = useRef(false);
|
|
45
|
+
function show() {
|
|
46
|
+
const panel = panelRef.current;
|
|
47
|
+
if (!panel || isPopoverOpen(panel) || typeof panel.showPopover !== "function")
|
|
48
|
+
return;
|
|
49
|
+
onOpen?.();
|
|
50
|
+
// Shown before its content renders: a hidden popover cannot take focus.
|
|
51
|
+
panel.showPopover();
|
|
52
|
+
setOpen(true);
|
|
53
|
+
}
|
|
54
|
+
function close() {
|
|
55
|
+
if (isPopoverOpen(panelRef.current))
|
|
56
|
+
panelRef.current?.hidePopover();
|
|
57
|
+
}
|
|
58
|
+
useImperativeHandle(ref, () => ({ open: show, close }));
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
const panel = panelRef.current;
|
|
61
|
+
const button = buttonRef.current;
|
|
62
|
+
if (!open || !panel || !button)
|
|
63
|
+
return;
|
|
64
|
+
const place = () => placeUnderAnchor(panel, button.getBoundingClientRect());
|
|
65
|
+
place();
|
|
66
|
+
(panel.querySelector("[data-autofocus]") ?? panel.querySelector("input, button"))?.focus();
|
|
67
|
+
return followAnchor(place);
|
|
68
|
+
}, [open]);
|
|
69
|
+
function onToggle(event) {
|
|
70
|
+
if (event.newState === "closed")
|
|
71
|
+
setOpen(false);
|
|
72
|
+
}
|
|
73
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", className: cx("nx-editor__tool", className), tabIndex: tabIndex, "aria-label": label, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, title: shortcut ? `${label} (${shortcut})` : label, ...rest, onPointerDown: () => {
|
|
74
|
+
dismissingRef.current = isPopoverOpen(panelRef.current);
|
|
75
|
+
},
|
|
76
|
+
// The editor keeps its focus and selection.
|
|
77
|
+
onMouseDown: event => event.preventDefault(), onClick: () => {
|
|
78
|
+
if (dismissingRef.current)
|
|
79
|
+
dismissingRef.current = false;
|
|
80
|
+
else if (isPopoverOpen(panelRef.current))
|
|
81
|
+
close();
|
|
82
|
+
else
|
|
83
|
+
show();
|
|
84
|
+
}, children: icon }), _jsx("div", { ref: panelRef, id: id, popover: "auto", role: "dialog", "aria-label": label, className: "nx-popover nx-rte-panel", onToggle: onToggle, children: open && children(close) })] }));
|
|
85
|
+
}
|
|
86
|
+
/** Text colors and highlights, each a grid of swatches: arrow keys move, Enter or a click picks. */
|
|
87
|
+
export function ColorPanel({ color, background, onPick }) {
|
|
88
|
+
return (_jsxs("div", { className: "nx-rte-colors", children: [_jsx(SwatchGrid, { title: "Text color", kind: "color", colors: RICH_TEXT_COLORS, current: color, onPick: onPick, autofocus: true }), _jsx(SwatchGrid, { title: "Highlight", kind: "background", colors: RICH_TEXT_HIGHLIGHTS, current: background, onPick: onPick })] }));
|
|
89
|
+
}
|
|
90
|
+
const SWATCH_COLUMNS = 5;
|
|
91
|
+
function SwatchGrid({ title, kind, colors, current, onPick, autofocus }) {
|
|
92
|
+
const titleId = useId();
|
|
93
|
+
const selected = Math.max(0, colors.findIndex(color => color.value === current));
|
|
94
|
+
function onKeyDown(event) {
|
|
95
|
+
const buttons = [...event.currentTarget.querySelectorAll("button")];
|
|
96
|
+
const index = buttons.indexOf(document.activeElement);
|
|
97
|
+
const next = event.key === "ArrowRight" ? index + 1
|
|
98
|
+
: event.key === "ArrowLeft" ? index - 1
|
|
99
|
+
: event.key === "ArrowDown" ? index + SWATCH_COLUMNS
|
|
100
|
+
: event.key === "ArrowUp" ? index - SWATCH_COLUMNS
|
|
101
|
+
: event.key === "Home" ? 0
|
|
102
|
+
: event.key === "End" ? buttons.length - 1
|
|
103
|
+
: null;
|
|
104
|
+
if (next === null || index < 0)
|
|
105
|
+
return;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
const target = buttons[Math.min(Math.max(next, 0), buttons.length - 1)];
|
|
108
|
+
for (const button of buttons)
|
|
109
|
+
button.tabIndex = button === target ? 0 : -1;
|
|
110
|
+
target.focus();
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs("div", { className: "nx-rte-colors__group", children: [_jsx("div", { id: titleId, className: "nx-rte-colors__title", children: title }), _jsx("div", { role: "group", "aria-labelledby": titleId, className: "nx-rte-colors__grid", onKeyDown: onKeyDown, children: colors.map((color, index) => {
|
|
113
|
+
const isSelected = index === selected;
|
|
114
|
+
const style = { "--nx-swatch": color.value ? (color.dark ? `light-dark(${color.value}, ${color.dark})` : color.value) : undefined };
|
|
115
|
+
return (_jsx("button", { type: "button", className: "nx-rte-colors__swatch", "data-kind": kind, "data-none": color.value ? undefined : "", "data-autofocus": autofocus && isSelected ? "" : undefined, "aria-label": color.value ? `${title}: ${color.name}` : color.name === "Default" ? "Default text color" : "No highlight", "aria-pressed": isSelected, title: color.value ? `${color.name} ${color.value}` : color.name, tabIndex: isSelected ? 0 : -1, style: style, onClick: () => onPick(kind, color.value), children: kind === "color" && color.value && _jsx("span", { "aria-hidden": "true", children: "A" }) }, color.name));
|
|
116
|
+
}) })] }));
|
|
117
|
+
}
|
|
118
|
+
/** The link box: the address (and the text, when nothing is selected), with Apply, Remove, and Cancel. */
|
|
119
|
+
export function LinkPanel({ href, askText, onApply, onRemove, onCancel }) {
|
|
120
|
+
const formId = useId();
|
|
121
|
+
const [error, setError] = useState(null);
|
|
122
|
+
const urlRef = useRef(null);
|
|
123
|
+
const textRef = useRef(null);
|
|
124
|
+
function apply(event) {
|
|
125
|
+
event?.preventDefault();
|
|
126
|
+
const url = normalizeLink(urlRef.current?.value ?? "");
|
|
127
|
+
if (!url) {
|
|
128
|
+
setError("Enter a web address, an email, or a phone number.");
|
|
129
|
+
urlRef.current?.focus();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
onApply(url, textRef.current?.value.trim() ?? "");
|
|
133
|
+
}
|
|
134
|
+
// Enter applies. The inputs belong to no form, so Enter never submits the form around the editor.
|
|
135
|
+
function onKeyDown(event) {
|
|
136
|
+
if (event.key === "Enter" && event.target.tagName === "INPUT")
|
|
137
|
+
apply(event);
|
|
138
|
+
}
|
|
139
|
+
return (_jsxs("div", { className: "nx-rte-link", onKeyDown: onKeyDown, children: [_jsx(TextBox, { ref: urlRef, form: formId, size: "sm", label: "Link address", placeholder: "https://", defaultValue: href ?? "", inputMode: "url", autoComplete: "off", spellCheck: false, error: error, onValueChange: () => setError(null), "data-autofocus": "" }), askText && _jsx(TextBox, { ref: textRef, form: formId, size: "sm", label: "Text", placeholder: "Shown instead of the address", autoComplete: "off" }), _jsxs("div", { className: "nx-rte-link__actions", children: [href && (_jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconLinkOff }), onClick: onRemove, children: "Remove" })), _jsx("span", { className: "nx-rte-link__spacer" }), _jsx(Button, { size: "sm", onClick: onCancel, children: "Cancel" }), _jsx(Button, { size: "sm", variant: "primary", onClick: () => apply(), children: "Apply" })] })] }));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A typed address as a link: "nexus.com" gets https://, an email gets mailto:, a phone number gets tel:. `null` when it
|
|
143
|
+
* is empty or not safe (`javascript:` and the like).
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeLink(input) {
|
|
146
|
+
const text = input.trim();
|
|
147
|
+
if (!text)
|
|
148
|
+
return null;
|
|
149
|
+
let url = text;
|
|
150
|
+
if (/^[^\s@/:]+@[^\s@/:]+\.[^\s@/:]+$/.test(text))
|
|
151
|
+
url = `mailto:${text}`;
|
|
152
|
+
else if (/^\+?[\d\s().-]{6,}$/.test(text) && /\d{3}/.test(text))
|
|
153
|
+
url = `tel:${text.replace(/[\s().-]/g, "")}`;
|
|
154
|
+
else if (/^(www\.|[a-z0-9-]+(\.[a-z0-9-]+)+([/:?#]|$))/i.test(text) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(text))
|
|
155
|
+
url = `https://${text}`;
|
|
156
|
+
return safeUrl(url);
|
|
157
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type FormEvent, type ReactNode } from "react";
|
|
2
|
+
import { RowsStore, type RowsSnapshot } from "../../Helpers/RowsStore.ts";
|
|
3
|
+
import type { InputParams, RowsInputProps, RowValue } from "../../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
|
|
5
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
6
|
+
/** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
|
|
7
|
+
export declare const ROW_INPUT_NAME = "nx-rows:";
|
|
8
|
+
/** Where focus goes in a row once it is on the page: an input by name, the row's first input, or its actions button. */
|
|
9
|
+
export type RowFocusTarget = string | {
|
|
10
|
+
first: true;
|
|
11
|
+
} | {
|
|
12
|
+
actions: true;
|
|
13
|
+
};
|
|
14
|
+
export interface RowsLayout {
|
|
15
|
+
/** Brings a row onto the page if needed and focuses a part of it. */
|
|
16
|
+
focusRow: (key: string, target: RowFocusTarget) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The store behind a tabular or group form, kept in step with the props: rules, a controlled value, the value
|
|
20
|
+
* listener, a parent form's data and reset, and the handle given to `ref`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useRowsInput<TRow extends RowValue>(props: RowsInputProps<TRow>, fallbackItem: string, layoutRef: {
|
|
23
|
+
current: RowsLayout | null;
|
|
24
|
+
}): {
|
|
25
|
+
store: RowsStore<TRow>;
|
|
26
|
+
snapshot: RowsSnapshot;
|
|
27
|
+
proxyRef: import("react").RefObject<HTMLInputElement | null>;
|
|
28
|
+
onInvalid: (event: FormEvent<HTMLInputElement>) => void;
|
|
29
|
+
canAdd: boolean;
|
|
30
|
+
actions: {
|
|
31
|
+
add(values?: Partial<TRow>, index?: number): string;
|
|
32
|
+
remove(key: string): void;
|
|
33
|
+
duplicate(key: string): void;
|
|
34
|
+
move(key: string, delta: number): void;
|
|
35
|
+
undo(): void;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** The first control of an element that takes focus. */
|
|
39
|
+
export declare function firstFocusable(element: Element | null): HTMLElement | null;
|
|
40
|
+
/** Subscribes a row's component to its row: it renders again only when the row's messages or code-set values change. */
|
|
41
|
+
export declare function useRowVersion(store: RowsStore<RowValue>, key: string): number;
|
|
42
|
+
/** A row's number and name for labels: "row 2", "address 2". */
|
|
43
|
+
export declare function rowName(store: RowsStore<RowValue>, index: number): string;
|
|
44
|
+
/** The text of an input's label, for names built from it. */
|
|
45
|
+
export declare function labelText(params: InputParams): string;
|
|
46
|
+
export interface RowCellProps {
|
|
47
|
+
store: RowsStore<RowValue>;
|
|
48
|
+
rowKey: string;
|
|
49
|
+
params: InputParams;
|
|
50
|
+
index: number;
|
|
51
|
+
error: string | undefined;
|
|
52
|
+
layout: "tabular" | "group";
|
|
53
|
+
idPrefix: string;
|
|
54
|
+
size: Size;
|
|
55
|
+
design?: InputDesign;
|
|
56
|
+
readOnly: boolean;
|
|
57
|
+
disabled: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
|
|
61
|
+
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
62
|
+
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
63
|
+
*/
|
|
64
|
+
export declare const RowCell: import("react").MemoExoticComponent<({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled }: RowCellProps) => ReactNode>;
|
|
65
|
+
/** The legend of the rows, with the required mark and, when read-only, a lock. */
|
|
66
|
+
export declare function RowsLegend({ id, label, required, lock }: {
|
|
67
|
+
id: string;
|
|
68
|
+
label: ReactNode;
|
|
69
|
+
required?: boolean;
|
|
70
|
+
lock?: ReactNode;
|
|
71
|
+
}): import("react").JSX.Element | null;
|
|
72
|
+
export interface RowsFooterProps {
|
|
73
|
+
messageId: string;
|
|
74
|
+
store: RowsStore<RowValue>;
|
|
75
|
+
snapshot: RowsSnapshot;
|
|
76
|
+
/** An error from outside, shown first. */
|
|
77
|
+
error?: string | null;
|
|
78
|
+
hint?: ReactNode;
|
|
79
|
+
/** Replaces the hint while it is set: in a table, the message or hint of the input with focus. */
|
|
80
|
+
status?: {
|
|
81
|
+
text: string;
|
|
82
|
+
error: boolean;
|
|
83
|
+
} | null;
|
|
84
|
+
addLabel: string;
|
|
85
|
+
addable: boolean;
|
|
86
|
+
canAdd: boolean;
|
|
87
|
+
max?: number;
|
|
88
|
+
maxMessage: string;
|
|
89
|
+
size: Size;
|
|
90
|
+
onAdd: () => void;
|
|
91
|
+
onUndo: () => void;
|
|
92
|
+
}
|
|
93
|
+
/** The line under the rows: Add, the rows' message or hint (or a removed row with Undo), and the count against `max`. */
|
|
94
|
+
export declare function RowsFooter({ messageId, store, snapshot, error, hint, status, addLabel, addable, canAdd, max, maxMessage, size, onAdd, onUndo }: RowsFooterProps): import("react").JSX.Element;
|
|
95
|
+
/** Ids, names, and read-only feedback for a rows input's frame. */
|
|
96
|
+
export declare function useRowsFrame(props: RowsInputProps<RowValue>, store: RowsStore<RowValue>): {
|
|
97
|
+
id: string;
|
|
98
|
+
legendId: string;
|
|
99
|
+
messageId: string;
|
|
100
|
+
lock: ReactNode;
|
|
101
|
+
addLabel: string;
|
|
102
|
+
emptyMessage: string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined>;
|
|
103
|
+
maxMessage: string;
|
|
104
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
7
|
+
import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
|
|
8
|
+
import { RowsStore } from "../../Helpers/RowsStore.js";
|
|
9
|
+
import { Button } from "../Documents/Button.js";
|
|
10
|
+
import { hasContent } from "./Field.js";
|
|
11
|
+
import { isFirstInvalid } from "./InputField.js";
|
|
12
|
+
import { renderInput } from "./InputRenderer.js";
|
|
13
|
+
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
14
|
+
/*
|
|
15
|
+
* What TabularForm and GroupForm share: the store and its link to the page (the value, a parent form's data, reset, and
|
|
16
|
+
* submit), the cell that renders one input of one row, the legend, and the line under the rows. The two differ only
|
|
17
|
+
* in how they lay the rows out.
|
|
18
|
+
*/
|
|
19
|
+
/** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
|
|
20
|
+
export const ROW_INPUT_NAME = "nx-rows:";
|
|
21
|
+
function sameRows(a, b) {
|
|
22
|
+
return a.length === b.length && a.every((row, index) => row === b[index]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The store behind a tabular or group form, kept in step with the props: rules, a controlled value, the value
|
|
26
|
+
* listener, a parent form's data and reset, and the handle given to `ref`.
|
|
27
|
+
*/
|
|
28
|
+
export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
29
|
+
const { inputs, required, min, max, unique, validate, rowKey, newRow, onRowChange, itemLabel, itemLabelPlural, value, defaultValue, name, readOnly = false, disabled = false, ref } = props;
|
|
30
|
+
const preferences = useDatePreferences();
|
|
31
|
+
const options = { inputs, required, min, max, unique, validate, rowKey, newRow, onRowChange, itemLabel, itemLabelPlural, fallbackItem, context: { preferences } };
|
|
32
|
+
const [store] = useState(() => new RowsStore(options, value ?? defaultValue ?? []));
|
|
33
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
34
|
+
const proxyRef = useRef(null);
|
|
35
|
+
const latest = useRef({ props, readOnly, disabled });
|
|
36
|
+
latest.current = { props, readOnly, disabled };
|
|
37
|
+
// Rules that change what is valid check every row again; callbacks only swap.
|
|
38
|
+
const rulesKey = [inputs, required, min, max, unique?.join(","), itemLabel, itemLabelPlural, preferences];
|
|
39
|
+
const rulesRef = useRef(rulesKey);
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const changed = rulesKey.some((item, index) => !Object.is(item, rulesRef.current[index]));
|
|
42
|
+
rulesRef.current = rulesKey;
|
|
43
|
+
store.configure(options, changed);
|
|
44
|
+
});
|
|
45
|
+
// A controlled value that is not the one the store gave loads anew. The same rows in a new array change nothing.
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
if (value === undefined || value === store.lastValue || (store.lastValue && sameRows(value, store.lastValue)))
|
|
48
|
+
return;
|
|
49
|
+
store.replace(value);
|
|
50
|
+
}, [store, value]);
|
|
51
|
+
/** The form's submit is blocked while any row has a problem, with the first message; read-only rows are not checked. */
|
|
52
|
+
const syncValidity = useCallback(() => {
|
|
53
|
+
const proxy = proxyRef.current;
|
|
54
|
+
if (!proxy)
|
|
55
|
+
return;
|
|
56
|
+
const invalid = !latest.current.readOnly && store.hasProblems();
|
|
57
|
+
proxy.setCustomValidity(invalid ? (store.firstMessage() ?? "Check the rows.") : "");
|
|
58
|
+
}, [store]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
syncValidity();
|
|
61
|
+
return store.subscribeValue(rows => {
|
|
62
|
+
syncValidity();
|
|
63
|
+
latest.current.props.onValueChange?.(rows);
|
|
64
|
+
});
|
|
65
|
+
}, [store, syncValidity]);
|
|
66
|
+
useEffect(syncValidity);
|
|
67
|
+
// Inside a form: the rows go into its data as JSON under `name`, and a reset brings back the rows loaded last.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const form = proxyRef.current?.form;
|
|
70
|
+
if (!form)
|
|
71
|
+
return;
|
|
72
|
+
const onFormData = (event) => {
|
|
73
|
+
for (const key of [...event.formData.keys()])
|
|
74
|
+
if (key.startsWith(ROW_INPUT_NAME))
|
|
75
|
+
event.formData.delete(key);
|
|
76
|
+
const fieldName = latest.current.props.name;
|
|
77
|
+
if (fieldName && !latest.current.disabled)
|
|
78
|
+
event.formData.set(fieldName, JSON.stringify(store.getValue()));
|
|
79
|
+
};
|
|
80
|
+
// The reset event comes before the browser restores the fields.
|
|
81
|
+
const onReset = () => setTimeout(() => store.reset());
|
|
82
|
+
form.addEventListener("formdata", onFormData);
|
|
83
|
+
form.addEventListener("reset", onReset);
|
|
84
|
+
return () => {
|
|
85
|
+
form.removeEventListener("formdata", onFormData);
|
|
86
|
+
form.removeEventListener("reset", onReset);
|
|
87
|
+
};
|
|
88
|
+
}, [store, props.form, name]);
|
|
89
|
+
function focusProblem(place) {
|
|
90
|
+
const layout = layoutRef.current;
|
|
91
|
+
if (!place || !layout)
|
|
92
|
+
return;
|
|
93
|
+
if (place.key !== null)
|
|
94
|
+
layout.focusRow(place.key, place.name);
|
|
95
|
+
else
|
|
96
|
+
(addButtonOf(proxyRef.current) ?? proxyRef.current?.closest("fieldset"))?.focus();
|
|
97
|
+
}
|
|
98
|
+
/** A form's submit found the rows invalid: every message shows, and the first problem takes focus. */
|
|
99
|
+
function onInvalid(event) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
const place = store.validateAll();
|
|
102
|
+
if (isFirstInvalid(event.currentTarget))
|
|
103
|
+
focusProblem(place);
|
|
104
|
+
}
|
|
105
|
+
const canAdd = !readOnly && !disabled && props.addable !== false && (max === undefined || snapshot.keys.length < max);
|
|
106
|
+
const actions = {
|
|
107
|
+
add(values, index) {
|
|
108
|
+
const key = store.add(values, index);
|
|
109
|
+
layoutRef.current?.focusRow(key, { first: true });
|
|
110
|
+
return key;
|
|
111
|
+
},
|
|
112
|
+
remove(key) {
|
|
113
|
+
store.remove(key);
|
|
114
|
+
// The Undo button takes focus, so the keyboard can bring the row straight back.
|
|
115
|
+
requestAnimationFrame(() => undoButtonOf(proxyRef.current)?.focus());
|
|
116
|
+
},
|
|
117
|
+
duplicate(key) {
|
|
118
|
+
const copy = store.duplicate(key);
|
|
119
|
+
if (copy)
|
|
120
|
+
layoutRef.current?.focusRow(copy, { first: true });
|
|
121
|
+
},
|
|
122
|
+
move(key, delta) {
|
|
123
|
+
store.move(key, store.indexOf(key) + delta);
|
|
124
|
+
layoutRef.current?.focusRow(key, { actions: true });
|
|
125
|
+
},
|
|
126
|
+
undo() {
|
|
127
|
+
const key = store.undoRemove();
|
|
128
|
+
if (key)
|
|
129
|
+
layoutRef.current?.focusRow(key, { first: true });
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
useImperativeHandle(ref, () => ({
|
|
133
|
+
getValue: () => store.getValue(),
|
|
134
|
+
setValue: rows => store.replace(rows, { emit: true }),
|
|
135
|
+
add: (values, index) => {
|
|
136
|
+
actions.add(values, index);
|
|
137
|
+
},
|
|
138
|
+
remove: index => {
|
|
139
|
+
const key = store.keyAt(index);
|
|
140
|
+
if (key)
|
|
141
|
+
store.remove(key);
|
|
142
|
+
},
|
|
143
|
+
validate: () => {
|
|
144
|
+
const place = store.validateAll();
|
|
145
|
+
focusProblem(place);
|
|
146
|
+
return place === null;
|
|
147
|
+
},
|
|
148
|
+
focusRow: (index, inputName) => {
|
|
149
|
+
const key = store.keyAt(index);
|
|
150
|
+
if (key)
|
|
151
|
+
layoutRef.current?.focusRow(key, inputName ?? { first: true });
|
|
152
|
+
},
|
|
153
|
+
focus: () => {
|
|
154
|
+
const first = store.keyAt(0);
|
|
155
|
+
if (first)
|
|
156
|
+
layoutRef.current?.focusRow(first, { first: true });
|
|
157
|
+
else
|
|
158
|
+
addButtonOf(proxyRef.current)?.focus();
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
|
+
[store]);
|
|
163
|
+
return { store, snapshot, proxyRef, onInvalid, canAdd, actions };
|
|
164
|
+
}
|
|
165
|
+
function addButtonOf(proxy) {
|
|
166
|
+
return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__add:not(:disabled)") ?? null;
|
|
167
|
+
}
|
|
168
|
+
function undoButtonOf(proxy) {
|
|
169
|
+
return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__undo") ?? null;
|
|
170
|
+
}
|
|
171
|
+
/** The first control of an element that takes focus. */
|
|
172
|
+
export function firstFocusable(element) {
|
|
173
|
+
return (element?.querySelector('input:not([type="hidden"]):not(:disabled):not(.nx-visually-hidden), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])') ?? null);
|
|
174
|
+
}
|
|
175
|
+
/** Subscribes a row's component to its row: it renders again only when the row's messages or code-set values change. */
|
|
176
|
+
export function useRowVersion(store, key) {
|
|
177
|
+
const subscribe = useCallback((listener) => store.subscribeRow(key, listener), [store, key]);
|
|
178
|
+
const read = () => store.version(key);
|
|
179
|
+
return useSyncExternalStore(subscribe, read, read);
|
|
180
|
+
}
|
|
181
|
+
/** A row's number and name for labels: "row 2", "address 2". */
|
|
182
|
+
export function rowName(store, index) {
|
|
183
|
+
return `${store.names.item} ${index + 1}`;
|
|
184
|
+
}
|
|
185
|
+
/** The text of an input's label, for names built from it. */
|
|
186
|
+
export function labelText(params) {
|
|
187
|
+
return typeof params.label === "string" ? params.label : (inputFieldName(params) ?? params.name);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
|
|
191
|
+
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
192
|
+
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
193
|
+
*/
|
|
194
|
+
export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled }) {
|
|
195
|
+
const [initial] = useState(() => store.get(rowKey, params.name));
|
|
196
|
+
const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
|
|
197
|
+
const needsName = params.type === "radio-group" || params.type === "checkbox-group";
|
|
198
|
+
const overrides = {
|
|
199
|
+
id: `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${params.name}`,
|
|
200
|
+
// Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
|
|
201
|
+
name: needsName ? `${ROW_INPUT_NAME}${idPrefix}:${rowKey}:${params.name}` : undefined,
|
|
202
|
+
defaultValue: initial,
|
|
203
|
+
onValueChange,
|
|
204
|
+
size: params.size ?? size,
|
|
205
|
+
readOnly,
|
|
206
|
+
disabled,
|
|
207
|
+
};
|
|
208
|
+
if (error !== undefined)
|
|
209
|
+
overrides.error = error;
|
|
210
|
+
// A required radio means one row has it on, which the rows check; each radio alone is not required.
|
|
211
|
+
if (params.type === "radio")
|
|
212
|
+
overrides.required = false;
|
|
213
|
+
if (layout === "tabular") {
|
|
214
|
+
overrides.label = undefined;
|
|
215
|
+
overrides.fieldName = inputFieldName(params);
|
|
216
|
+
overrides.design = "standard";
|
|
217
|
+
overrides["aria-label"] = `${labelText(params)}, ${rowName(store, index)}`;
|
|
218
|
+
}
|
|
219
|
+
else if (design && !params.design) {
|
|
220
|
+
overrides.design = design;
|
|
221
|
+
}
|
|
222
|
+
return renderInput(params, overrides);
|
|
223
|
+
});
|
|
224
|
+
/** The legend of the rows, with the required mark and, when read-only, a lock. */
|
|
225
|
+
export function RowsLegend({ id, label, required, lock }) {
|
|
226
|
+
if (!hasContent(label))
|
|
227
|
+
return null;
|
|
228
|
+
return (_jsxs("legend", { id: id, className: "nx-field__label nx-rows__legend", children: [label, required && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), lock] }));
|
|
229
|
+
}
|
|
230
|
+
/** The line under the rows: Add, the rows' message or hint (or a removed row with Undo), and the count against `max`. */
|
|
231
|
+
export function RowsFooter({ messageId, store, snapshot, error, hint, status, addLabel, addable, canAdd, max, maxMessage, size, onAdd, onUndo }) {
|
|
232
|
+
const count = snapshot.keys.length;
|
|
233
|
+
const message = error || snapshot.message;
|
|
234
|
+
const invalidRows = !message && snapshot.invalidRows > 0 ? snapshot.invalidRows : 0;
|
|
235
|
+
const shown = status?.text ? status : message ? { text: message, error: true } : invalidRows ? { text: `${invalidRows} ${invalidRows === 1 ? `${store.names.item} needs` : `${store.names.items} need`} attention. ${store.firstMessage() ?? ""}`, error: true } : null;
|
|
236
|
+
const buttonSize = size === "lg" ? "md" : "sm";
|
|
237
|
+
return (_jsxs("div", { className: "nx-rows__footer", children: [addable && (_jsx(Button, { size: buttonSize, className: "nx-rows__add", icon: _jsx(InlineIcon, { icon: iconPlus }), disabled: !canAdd, title: !canAdd && max !== undefined && count >= max ? maxMessage : undefined, onClick: onAdd, children: addLabel })), snapshot.removed ? (_jsxs("p", { className: "nx-rows__message nx-rows__removed", role: "status", children: [_jsxs("span", { children: [store.names.title, " ", snapshot.removed.index + 1, " removed."] }), _jsx(Button, { size: "sm", variant: "ghost", className: "nx-rows__undo", icon: _jsx(InlineIcon, { icon: iconArrowBackUp }), onClick: onUndo, children: "Undo" })] })) : shown || hasContent(hint) ? (_jsxs("p", { id: messageId, className: cx("nx-rows__message", "nx-field__message", shown?.error && "nx-field__message--error"), children: [shown?.error && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: shown ? shown.text : hint })] })) : null, max !== undefined && (_jsxs("span", { className: "nx-rows__count", "aria-label": `${count} of at most ${max}`, children: [count, "/", max] }))] }));
|
|
238
|
+
}
|
|
239
|
+
/** Ids, names, and read-only feedback for a rows input's frame. */
|
|
240
|
+
export function useRowsFrame(props, store) {
|
|
241
|
+
const generatedId = useId();
|
|
242
|
+
const id = props.id ?? generatedId;
|
|
243
|
+
const { readOnly = false, disabled = false } = props;
|
|
244
|
+
const legendRef = useRef(null);
|
|
245
|
+
const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, legendRef, props.readOnlyMessage ?? "Read-only: these rows cannot be changed.", {
|
|
246
|
+
lock: "label",
|
|
247
|
+
typing: false,
|
|
248
|
+
});
|
|
249
|
+
const names = store.names;
|
|
250
|
+
return {
|
|
251
|
+
id,
|
|
252
|
+
legendId: `${id}-legend`,
|
|
253
|
+
messageId: `${id}-message`,
|
|
254
|
+
lock: readOnlyNotice.lock,
|
|
255
|
+
addLabel: props.addLabel ?? `Add ${names.item}`,
|
|
256
|
+
emptyMessage: props.emptyMessage ?? `No ${names.items} yet.`,
|
|
257
|
+
maxMessage: props.max === undefined ? "" : `At most ${props.max} ${props.max === 1 ? names.item : names.items}.`,
|
|
258
|
+
};
|
|
259
|
+
}
|