nexus-shared 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +182 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +158 -0
- package/dist/Helpers/FileAdapter.js +173 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-icons";
|
|
4
|
-
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
6
|
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
7
|
+
import { browserInputMemory, FORM_SAVED_EVENT } from "../../Helpers/InputMemory.js";
|
|
8
|
+
import { narrowingValue, narrowInputParamsBy } from "../../Helpers/InputNarrowing.js";
|
|
7
9
|
import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
|
|
8
10
|
import { RowsStore } from "../../Helpers/RowsStore.js";
|
|
11
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
9
12
|
import { Button } from "../Documents/Button.js";
|
|
13
|
+
import { ComputedInput } from "./ComputedInput.js";
|
|
10
14
|
import { hasContent } from "./Field.js";
|
|
11
15
|
import { isFirstInvalid } from "./InputField.js";
|
|
12
16
|
import { renderInput } from "./InputRenderer.js";
|
|
@@ -64,7 +68,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
64
68
|
});
|
|
65
69
|
}, [store, syncValidity]);
|
|
66
70
|
useEffect(syncValidity);
|
|
67
|
-
//
|
|
71
|
+
// Remembered row inputs (`remember`) read and keep their values in this browser: rows added from now on open on them.
|
|
72
|
+
// Given once drawn, so the server's draw and the first one in the browser are the same.
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
if (inputs.some(params => params.remember))
|
|
75
|
+
store.attachMemory(browserInputMemory());
|
|
76
|
+
}, [store, inputs]);
|
|
77
|
+
// Inside a form: the rows go into its data as JSON under `name`, a reset brings back the rows loaded last, and a save
|
|
78
|
+
// (a SubmitForm's) keeps the remembered values of the row added last.
|
|
68
79
|
useEffect(() => {
|
|
69
80
|
const form = proxyRef.current?.form;
|
|
70
81
|
if (!form)
|
|
@@ -79,11 +90,17 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
79
90
|
};
|
|
80
91
|
// The reset event comes before the browser restores the fields.
|
|
81
92
|
const onReset = () => setTimeout(() => store.reset());
|
|
93
|
+
const onSaved = () => {
|
|
94
|
+
if (!latest.current.disabled)
|
|
95
|
+
store.rememberAdded();
|
|
96
|
+
};
|
|
82
97
|
form.addEventListener("formdata", onFormData);
|
|
83
98
|
form.addEventListener("reset", onReset);
|
|
99
|
+
form.addEventListener(FORM_SAVED_EVENT, onSaved);
|
|
84
100
|
return () => {
|
|
85
101
|
form.removeEventListener("formdata", onFormData);
|
|
86
102
|
form.removeEventListener("reset", onReset);
|
|
103
|
+
form.removeEventListener(FORM_SAVED_EVENT, onSaved);
|
|
87
104
|
};
|
|
88
105
|
}, [store, props.form, name]);
|
|
89
106
|
function focusProblem(place) {
|
|
@@ -179,9 +196,43 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
179
196
|
const row = store.row(key);
|
|
180
197
|
return row ? ask(row, name, index) : undefined;
|
|
181
198
|
}, [store, rowLock]);
|
|
199
|
+
// The form's values the rows follow (`narrowedBy` `from: "form"`): when one changes, every row draws again, narrowed afresh.
|
|
200
|
+
const formKey = formValuesKey(props.formValues);
|
|
201
|
+
const shownFormKey = useRef(formKey);
|
|
202
|
+
useLayoutEffect(() => {
|
|
203
|
+
if (shownFormKey.current === formKey)
|
|
204
|
+
return;
|
|
205
|
+
shownFormKey.current = formKey;
|
|
206
|
+
store.redrawRows();
|
|
207
|
+
}, [store, formKey]);
|
|
208
|
+
// Row inputs worked out from others (`compute`) read the rows' `computeWith` as they draw: a new one draws every row.
|
|
209
|
+
const computeWith = useCallback(() => latest.current.props.computeWith, []);
|
|
210
|
+
const shownComputeWith = useRef(props.computeWith);
|
|
211
|
+
useLayoutEffect(() => {
|
|
212
|
+
if (Object.is(shownComputeWith.current, props.computeWith))
|
|
213
|
+
return;
|
|
214
|
+
shownComputeWith.current = props.computeWith;
|
|
215
|
+
if (inputs.some(params => params.compute))
|
|
216
|
+
store.redrawRows();
|
|
217
|
+
}, [store, inputs, props.computeWith]);
|
|
182
218
|
// A cell's own params, merged once per answer: the same answer gives the same object, so the cell does not draw again.
|
|
183
219
|
const merged = useRef(new WeakMap());
|
|
184
220
|
const cellParams = useCallback((key, params, index) => {
|
|
221
|
+
const own = ownCellParams(key, params, index);
|
|
222
|
+
// Options narrowed by the row's own values and the form's (`narrowedBy`): read as the row draws, which it does when
|
|
223
|
+
// one of them changes.
|
|
224
|
+
if (!params.narrowedBy)
|
|
225
|
+
return own;
|
|
226
|
+
const row = store.row(key);
|
|
227
|
+
const outer = latest.current.props.formValues;
|
|
228
|
+
return narrowInputParamsBy(own, narrowing => {
|
|
229
|
+
if (narrowing.from === "form")
|
|
230
|
+
return outer?.[narrowing.name] ?? { value: undefined };
|
|
231
|
+
return { value: row?.[narrowing.name], label: latest.current.props.inputs.find(input => input.name === narrowing.name)?.label };
|
|
232
|
+
});
|
|
233
|
+
}, [store]);
|
|
234
|
+
/** The column's params, or the row's own over them (`cellParams`). */
|
|
235
|
+
function ownCellParams(key, params, index) {
|
|
185
236
|
const ask = latest.current.props.cellParams;
|
|
186
237
|
if (!ask)
|
|
187
238
|
return params;
|
|
@@ -201,8 +252,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
201
252
|
byColumn.set(params, result);
|
|
202
253
|
}
|
|
203
254
|
return result;
|
|
204
|
-
}
|
|
205
|
-
return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams };
|
|
255
|
+
}
|
|
256
|
+
return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams, computeWith };
|
|
257
|
+
}
|
|
258
|
+
/** The form values rows follow, as text that changes only when one of them does (a new object with the same values does not). */
|
|
259
|
+
function formValuesKey(values) {
|
|
260
|
+
if (!values)
|
|
261
|
+
return "";
|
|
262
|
+
return JSON.stringify(Object.keys(values).sort().map(name => [name, narrowingValue(values[name]?.value)]));
|
|
206
263
|
}
|
|
207
264
|
function addButtonOf(proxy) {
|
|
208
265
|
return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__add:not(:disabled)") ?? null;
|
|
@@ -228,17 +285,23 @@ export function rowName(store, index) {
|
|
|
228
285
|
export function labelText(params) {
|
|
229
286
|
return typeof params.label === "string" ? params.label : (inputFieldName(params) ?? params.name);
|
|
230
287
|
}
|
|
288
|
+
/** A cell's control id: the rows' id, the row's key, and the input's name. */
|
|
289
|
+
const cellId = (idPrefix, rowKey, name) => `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${name}`;
|
|
231
290
|
/**
|
|
232
291
|
* One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
|
|
233
292
|
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
234
293
|
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
235
294
|
*/
|
|
236
|
-
export const RowCell = memo(function RowCell(
|
|
295
|
+
export const RowCell = memo(function RowCell(props) {
|
|
296
|
+
const { store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock } = props;
|
|
237
297
|
const [initial] = useState(() => store.get(rowKey, params.name));
|
|
238
298
|
const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
|
|
239
|
-
|
|
299
|
+
// Worked out from the row's other inputs: no value of its own, drawn from the row as it is now.
|
|
300
|
+
if (params.compute)
|
|
301
|
+
return _jsx(RowComputedCell, { ...props });
|
|
302
|
+
const needsName = params.type === EInputTypes.RadioGroup || params.type === EInputTypes.CheckBoxGroup;
|
|
240
303
|
const overrides = {
|
|
241
|
-
id:
|
|
304
|
+
id: cellId(idPrefix, rowKey, params.name),
|
|
242
305
|
// Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
|
|
243
306
|
name: needsName ? `${ROW_INPUT_NAME}${idPrefix}:${rowKey}:${params.name}` : undefined,
|
|
244
307
|
defaultValue: initial,
|
|
@@ -257,7 +320,7 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
|
|
|
257
320
|
if (error !== undefined)
|
|
258
321
|
overrides.error = error;
|
|
259
322
|
// A required radio means one row has it on, which the rows check; each radio alone is not required.
|
|
260
|
-
if (params.type ===
|
|
323
|
+
if (params.type === EInputTypes.Radio)
|
|
261
324
|
overrides.required = false;
|
|
262
325
|
if (layout === "tabular") {
|
|
263
326
|
overrides.label = undefined;
|
|
@@ -270,6 +333,31 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
|
|
|
270
333
|
}
|
|
271
334
|
return renderInput(params, overrides);
|
|
272
335
|
});
|
|
336
|
+
/**
|
|
337
|
+
* A row input worked out from the row's others (`compute`): `ComputedInput` reading the row and the rows' `computeWith`,
|
|
338
|
+
* drawn again whenever the row changes, and writing a typed-in figure's answer as the person's change of the row
|
|
339
|
+
* (`RowsStore.setFromPerson`). In a table it has no label and no line under it: the note goes under the rows.
|
|
340
|
+
*/
|
|
341
|
+
function RowComputedCell({ store, rowKey, params, index, layout, idPrefix, size, design, readOnly, disabled, lock, cells }) {
|
|
342
|
+
useRowVersion(store, rowKey);
|
|
343
|
+
const at = useRef(index);
|
|
344
|
+
at.current = index;
|
|
345
|
+
const read = useCallback(() => (store.row(rowKey) ?? {}), [store, rowKey]);
|
|
346
|
+
const write = useCallback((values) => void store.setFromPerson(rowKey, values), [store, rowKey]);
|
|
347
|
+
// The row's other inputs as this row has them: its own params (`cellParams`), and read-only where the cell is locked.
|
|
348
|
+
const inputParams = useCallback((name) => {
|
|
349
|
+
const column = store.inputs.find(params => params.name === name);
|
|
350
|
+
if (!column || !cells)
|
|
351
|
+
return column;
|
|
352
|
+
const own = cells.cellParams(rowKey, column, at.current);
|
|
353
|
+
return cells.cellLock(rowKey, name, at.current) ? { ...own, readOnly: true } : own;
|
|
354
|
+
}, [store, rowKey, cells]);
|
|
355
|
+
const sourceId = useCallback((name) => cellId(idPrefix, rowKey, name), [idPrefix, rowKey]);
|
|
356
|
+
const tabular = layout === "tabular";
|
|
357
|
+
const say = cells?.onNote;
|
|
358
|
+
const onNote = useMemo(() => (tabular && say ? (note) => say(rowKey, params.name, note) : undefined), [tabular, say, rowKey, params.name]);
|
|
359
|
+
return (_jsx(ComputedInput, { params: params, id: cellId(idPrefix, rowKey, params.name), read: read, withValue: cells?.computeWith?.(), write: write, inputParams: inputParams, sourceId: sourceId, size: params.size ?? size, design: design, readOnly: readOnly || Boolean(lock), readOnlyMessage: typeof lock === "string" ? lock : undefined, disabled: disabled, cell: tabular ? { "aria-label": `${labelText(params)}, ${rowName(store, index)}` } : undefined, onNote: onNote }));
|
|
360
|
+
}
|
|
273
361
|
/** The legend of the rows, with the required mark and, when read-only, a lock. */
|
|
274
362
|
export function RowsLegend({ id, label, required, lock }) {
|
|
275
363
|
if (!hasContent(label))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RowValue, type TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
4
4
|
* value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
|
|
@@ -18,10 +18,10 @@ import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces
|
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
|
|
21
|
-
* { type:
|
|
22
|
-
* { type:
|
|
23
|
-
* { type:
|
|
24
|
-
* { type:
|
|
21
|
+
* { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
|
|
22
|
+
* { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
|
|
23
|
+
* { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
|
|
24
|
+
* { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
|
|
25
25
|
* ]} />
|
|
26
26
|
*/
|
|
27
27
|
export declare function TabularForm<TRow extends RowValue = RowValue>(props: TabularFormProps<TRow>): import("react").JSX.Element;
|
|
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconLockOpen, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
|
|
7
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
6
8
|
import { Menu } from "../Documents/Menu.js";
|
|
7
9
|
import { firstFocusable, labelText, RowCell, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
|
|
8
10
|
/** Height of a row: the input's box and the line under it. Measured from the first row once it renders. */
|
|
@@ -12,38 +14,36 @@ const OVERSCAN = 8;
|
|
|
12
14
|
const DEFAULT_MAX_HEIGHT = 440;
|
|
13
15
|
/** A column's width by input type, when its params give none. */
|
|
14
16
|
const COLUMN_WIDTH = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
[EInputTypes.Text]: "10rem",
|
|
18
|
+
[EInputTypes.Email]: "13rem",
|
|
19
|
+
[EInputTypes.Password]: "10rem",
|
|
20
|
+
[EInputTypes.Search]: "10rem",
|
|
21
|
+
[EInputTypes.Tel]: "9rem",
|
|
22
|
+
[EInputTypes.Url]: "13rem",
|
|
23
|
+
[EInputTypes.TextArea]: "14rem",
|
|
24
|
+
[EInputTypes.Number]: "7.5rem",
|
|
25
|
+
[EInputTypes.CheckBox]: "6rem",
|
|
26
|
+
[EInputTypes.Switch]: "6rem",
|
|
27
|
+
[EInputTypes.Radio]: "6rem",
|
|
28
|
+
[EInputTypes.CheckBoxGroup]: "16rem",
|
|
29
|
+
[EInputTypes.RadioGroup]: "16rem",
|
|
30
|
+
[EInputTypes.Dropdown]: "10rem",
|
|
31
|
+
[EInputTypes.Date]: "10rem",
|
|
32
|
+
[EInputTypes.DateRange]: "15rem",
|
|
33
|
+
[EInputTypes.Time]: "8rem",
|
|
34
|
+
[EInputTypes.DateTime]: "14rem",
|
|
35
|
+
[EInputTypes.Slider]: "10rem",
|
|
36
|
+
[EInputTypes.RangeSlider]: "12rem",
|
|
35
37
|
};
|
|
36
|
-
/** Columns that keep their width instead of sharing the spare room. */
|
|
37
|
-
const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
|
|
38
38
|
/** What a locked row's cells say when one is used. */
|
|
39
39
|
const LOCKED_ROW = "Locked: unlock the row to change it.";
|
|
40
40
|
const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
|
|
41
41
|
const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
|
|
42
42
|
/** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
|
|
43
43
|
function cellKind(params) {
|
|
44
|
-
if (params.type ===
|
|
44
|
+
if (params.type === EInputTypes.Number)
|
|
45
45
|
return "number";
|
|
46
|
-
if (
|
|
46
|
+
if (isBooleanInput(params))
|
|
47
47
|
return "check";
|
|
48
48
|
return "text";
|
|
49
49
|
}
|
|
@@ -84,7 +84,7 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
|
|
|
84
84
|
return;
|
|
85
85
|
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
86
86
|
}
|
|
87
|
-
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
|
|
87
|
+
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index), cells: context }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
|
|
88
88
|
});
|
|
89
89
|
/**
|
|
90
90
|
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
@@ -105,10 +105,10 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
|
|
|
105
105
|
*
|
|
106
106
|
* @example
|
|
107
107
|
* <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
|
|
108
|
-
* { type:
|
|
109
|
-
* { type:
|
|
110
|
-
* { type:
|
|
111
|
-
* { type:
|
|
108
|
+
* { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
|
|
109
|
+
* { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
|
|
110
|
+
* { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
|
|
111
|
+
* { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
|
|
112
112
|
* ]} />
|
|
113
113
|
*/
|
|
114
114
|
export function TabularForm(props) {
|
|
@@ -135,6 +135,15 @@ export function TabularForm(props) {
|
|
|
135
135
|
const pageRows = Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight);
|
|
136
136
|
const [range, setRange] = useState({ start: 0, end: pageRows + OVERSCAN });
|
|
137
137
|
const [status, setStatus] = useState(null);
|
|
138
|
+
/** The note of the figure typed into a worked-out cell last (`compute.solve`), until it no longer applies. */
|
|
139
|
+
const [note, setNote] = useState(null);
|
|
140
|
+
const onNote = useCallback((key, name, next) => {
|
|
141
|
+
setNote(current => {
|
|
142
|
+
if (next)
|
|
143
|
+
return current && current.key === key && current.name === name && current.text === next.text && current.refused === next.refused ? current : { ...next, key, name };
|
|
144
|
+
return current && current.key === key && current.name === name ? null : current;
|
|
145
|
+
});
|
|
146
|
+
}, []);
|
|
138
147
|
const pendingRef = useRef(null);
|
|
139
148
|
const start = Math.min(range.start, count);
|
|
140
149
|
const end = Math.min(count, Math.max(range.end, start + 1));
|
|
@@ -311,7 +320,7 @@ export function TabularForm(props) {
|
|
|
311
320
|
layoutRef.current?.focusRow(key, name);
|
|
312
321
|
}
|
|
313
322
|
}
|
|
314
|
-
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked]);
|
|
323
|
+
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked, computeWith: rows.computeWith, onNote }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked, rows.computeWith, onNote]);
|
|
315
324
|
/**
|
|
316
325
|
* Which columns are generated for **every** row on the page. The lock is then said once, on the column header,
|
|
317
326
|
* instead of on each of its cells: eighteen locks down one column is eighteen times the ink for one fact. The cells
|
|
@@ -359,7 +368,7 @@ export function TabularForm(props) {
|
|
|
359
368
|
const template = [
|
|
360
369
|
numbered ? numberWidth : null,
|
|
361
370
|
...columns.map((params, index) => {
|
|
362
|
-
const grow = params.grow ?? (
|
|
371
|
+
const grow = params.grow ?? (isBooleanInput(params) ? 0 : 1);
|
|
363
372
|
return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
|
|
364
373
|
}),
|
|
365
374
|
actionsWidth,
|
|
@@ -369,6 +378,10 @@ export function TabularForm(props) {
|
|
|
369
378
|
const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, actionsWidth ?? "0px"].join(" + ")})`;
|
|
370
379
|
const tableStyle = { "--nx-tabular-columns": template, "--nx-tabular-num": numbered ? numberWidth : "0px", minWidth };
|
|
371
380
|
const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
|
|
381
|
+
// A typed-in figure's note, named with its row, says more than a hint; a message of the cell in focus says more still.
|
|
382
|
+
const noteRow = note ? store.indexOf(note.key) : -1;
|
|
383
|
+
const said = note && noteRow >= 0 ? { text: `${store.names.title} ${noteRow + 1}: ${note.text}`, error: note.refused } : null;
|
|
384
|
+
const footerStatus = status?.error ? status : (said ?? status);
|
|
372
385
|
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "data-actions-at": atEnd ? "end" : undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
|
|
373
386
|
// A focused cell scrolls clear of the actions fixed at the end, as it does of the number at the start.
|
|
374
387
|
style: { maxHeight: viewportMax ?? cssLength(maxHeight), scrollPaddingRight: actionsWidth ?? undefined }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
|
|
@@ -376,5 +389,5 @@ export function TabularForm(props) {
|
|
|
376
389
|
const locked = lockedColumns.get(params.name);
|
|
377
390
|
const reason = typeof locked === "string" ? locked : locked ? "Read-only" : undefined;
|
|
378
391
|
return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: reason ?? (typeof columnHint === "string" ? columnHint : undefined), "data-nx-locked": locked ? "" : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && !locked && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), locked && _jsx(InlineIcon, { icon: iconLock, className: "nx-tabular__head-lock", "aria-hidden": "true" })] }, params.name));
|
|
379
|
-
}), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status:
|
|
392
|
+
}), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: footerStatus, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
380
393
|
}
|
|
@@ -22,6 +22,12 @@ export interface TextAreaProps extends NativeTextAreaProps, TextRules {
|
|
|
22
22
|
maxRows?: number;
|
|
23
23
|
/** Shows the character count, and the limit when `maxLength` is set. */
|
|
24
24
|
showCount?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Text that is code (HTML, JSON): the monospace font, lines kept as typed (no wrapping, a long line scrolls sideways
|
|
27
|
+
* inside the box), a tab two spaces wide, and no spelling, capitals, corrections, suggestions or translation. Tab still
|
|
28
|
+
* leaves the box, so a keyboard is never trapped in it.
|
|
29
|
+
*/
|
|
30
|
+
code?: boolean;
|
|
25
31
|
/** What a read-only field says when someone tries to change it. Default "Read-only: this field cannot be changed." */
|
|
26
32
|
readOnlyMessage?: string;
|
|
27
33
|
/** Name used in messages. Default: the label, when it is text. */
|
|
@@ -5,13 +5,22 @@ import { Field } from "./Field.js";
|
|
|
5
5
|
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
6
6
|
import { useTextField } from "./TextField.js";
|
|
7
7
|
import { useInputDesign } from "../../Helpers/UserPreferences.js";
|
|
8
|
+
/** A `code` text area's attributes: lines kept as typed, and nothing that would change what was typed. A page's own props win. */
|
|
9
|
+
const CODE_PROPS = {
|
|
10
|
+
wrap: "off",
|
|
11
|
+
spellCheck: false,
|
|
12
|
+
autoCapitalize: "off",
|
|
13
|
+
autoCorrect: "off",
|
|
14
|
+
autoComplete: "off",
|
|
15
|
+
translate: "no",
|
|
16
|
+
};
|
|
8
17
|
/**
|
|
9
18
|
* A multi-line text input with the same label, hint, validation, and designs as `TextBox`.
|
|
10
19
|
*
|
|
11
20
|
* @example <TextArea label="Description" maxLength={500} showCount autoResize />
|
|
12
21
|
*/
|
|
13
22
|
export function TextArea(props) {
|
|
14
|
-
const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
|
|
23
|
+
const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, code = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
|
|
15
24
|
// The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
|
|
16
25
|
const design = useInputDesign(ownDesign, Boolean(label));
|
|
17
26
|
const field = useTextField(props);
|
|
@@ -30,5 +39,5 @@ export function TextArea(props) {
|
|
|
30
39
|
readOnlyNotice.hide();
|
|
31
40
|
field.controlProps.onBlur(event);
|
|
32
41
|
}
|
|
33
|
-
return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...(code ? CODE_PROPS : null), ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", code && "nx-field__control--code", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
|
|
34
43
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EInputTypes, type TextInputType } from "../../Interfaces/FormInterfaces.ts";
|
|
2
|
+
import type { TextBoxType } from "./TextBox.tsx";
|
|
3
|
+
/** The HTML type of each text box kind. */
|
|
4
|
+
export declare const TEXT_BOX_TYPES: Readonly<Record<TextInputType, TextBoxType>>;
|
|
5
|
+
/** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
|
|
6
|
+
export declare const isTextInputType: (type: EInputTypes) => type is TextInputType;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
2
|
+
/*
|
|
3
|
+
* Which input types a `TextBox` renders, and the HTML type each one gets: the one place an input type becomes a string.
|
|
4
|
+
* `renderInput` reads it; it is its own file, not exported from the package, so the unit suites can check it without
|
|
5
|
+
* a browser (scripts/test-form-store.mts).
|
|
6
|
+
*/
|
|
7
|
+
/** The HTML type of each text box kind. */
|
|
8
|
+
export const TEXT_BOX_TYPES = {
|
|
9
|
+
[EInputTypes.Text]: "text",
|
|
10
|
+
[EInputTypes.Email]: "email",
|
|
11
|
+
[EInputTypes.Password]: "password",
|
|
12
|
+
[EInputTypes.Search]: "search",
|
|
13
|
+
[EInputTypes.Tel]: "tel",
|
|
14
|
+
[EInputTypes.Url]: "url",
|
|
15
|
+
};
|
|
16
|
+
/** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
|
|
17
|
+
export const isTextInputType = (type) => Object.hasOwn(TEXT_BOX_TYPES, type);
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, type Ref } from "react";
|
|
2
|
-
import { type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
|
|
2
|
+
import { type TreeChangeRules, type TreeChildrenPage, type TreeChildrenRequest, type TreeFilter, type TreeItem, type TreeItemChange, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
|
|
3
|
+
import type { TableRowAction } from "../../Interfaces/TableInterfaces.ts";
|
|
3
4
|
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
4
5
|
export type TreeAction = "create" | "update" | "delete";
|
|
6
|
+
/**
|
|
7
|
+
* An action of the page's own on one item: an icon button on its row when `inline` (shown under the pointer or with
|
|
8
|
+
* focus, always on a touch screen), and an entry of the row's context menu either way. The row is busy while a
|
|
9
|
+
* promise `onSelect` returns runs.
|
|
10
|
+
*/
|
|
11
|
+
export interface TreeRowAction<TItem extends TreeItem = TreeItem> extends TableRowAction<TItem> {
|
|
12
|
+
/** The key that runs it on the focused row, as Insert adds, F2 renames and Delete deletes in a tree that edits its own items. */
|
|
13
|
+
shortcut?: "Insert" | "F2" | "Delete";
|
|
14
|
+
}
|
|
5
15
|
export interface TreeCreateRequest<TItem extends TreeItem = TreeItem> {
|
|
6
16
|
/** The item the new one goes under, or `null` at the top level. */
|
|
7
17
|
parent: TItem | null;
|
|
@@ -31,7 +41,10 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
|
|
|
31
41
|
/** Expands an item and its parents. */
|
|
32
42
|
expand: (value: TreeValue) => void;
|
|
33
43
|
collapse: (value: TreeValue) => void;
|
|
34
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Expands the parents of an item, scrolls to it, and focuses it unless `focus` is false. An item not loaded yet is
|
|
46
|
+
* revealed when the page that brings it comes (opened by `expand`, or Load more), unless the user moves on first.
|
|
47
|
+
*/
|
|
35
48
|
reveal: (value: TreeValue, focus?: boolean) => void;
|
|
36
49
|
getValue: () => TreeValue[];
|
|
37
50
|
/** Checks exactly these values, locked items included. For a tree without `value`. */
|
|
@@ -40,6 +53,8 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
|
|
|
40
53
|
checkAll: (checked: boolean) => void;
|
|
41
54
|
/** Every item in tree order, with the changes made in the tree. */
|
|
42
55
|
getItems: () => TItem[];
|
|
56
|
+
/** The item of a value, as the tree holds it now; `undefined` when it is not loaded. */
|
|
57
|
+
getItem: (value: TreeValue) => TItem | undefined;
|
|
43
58
|
/** Opens the editor for a new item under `parentValue`, or at the top level for `null`. */
|
|
44
59
|
startCreate: (parentValue: TreeValue | null) => void;
|
|
45
60
|
startRename: (value: TreeValue) => void;
|
|
@@ -53,6 +68,12 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
|
|
|
53
68
|
* when it scrolls in, a closed one when it is next expanded. What is drawn stays until the fresh pages replace it.
|
|
54
69
|
*/
|
|
55
70
|
refresh: (value?: TreeValue) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Writes changes made elsewhere into this tree, as `writeTreeChanges` does into every tree with a `cacheKey` (which
|
|
73
|
+
* calls this): items placed, moved and taken out, the parents' counts and chevrons, and focus kept on its row, or on the
|
|
74
|
+
* row in its place when the item left. `rules` gives `compare`, the order of siblings.
|
|
75
|
+
*/
|
|
76
|
+
apply: (changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>) => void;
|
|
56
77
|
}
|
|
57
78
|
export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
|
|
58
79
|
/**
|
|
@@ -159,6 +180,53 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
|
|
|
159
180
|
onItemsChange?: (items: TItem[]) => void;
|
|
160
181
|
/** Asks before deleting. Default true; needs `<PopupHost />` on the page. */
|
|
161
182
|
confirmDelete?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Actions of the page's own on each item - Add under it, Edit, Move to trash - for a tree whose items are records
|
|
185
|
+
* changed through forms rather than renamed in the row: icon buttons on the row for the `inline` ones, all of them in
|
|
186
|
+
* the row's context menu, and the key each names. Not offered on locked items, nor in a read-only or disabled tree.
|
|
187
|
+
*/
|
|
188
|
+
rowActions?: (item: TItem) => readonly TreeRowAction<TItem>[];
|
|
189
|
+
/**
|
|
190
|
+
* A button of the page's own at the end of the toolbar, such as Add New for a form. Its `description` is its tooltip
|
|
191
|
+
* and what a screen reader hears after its name. `disabled` with a `description` says why it cannot be pressed yet: it
|
|
192
|
+
* stays in the Tab order, looks unavailable, and a press - a click, a tap, Enter - shows the description in a note under
|
|
193
|
+
* it rather than doing nothing.
|
|
194
|
+
*/
|
|
195
|
+
add?: {
|
|
196
|
+
label: ReactNode;
|
|
197
|
+
onClick: () => void;
|
|
198
|
+
disabled?: boolean;
|
|
199
|
+
description?: string;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* The row shown as selected: the item the page picked - through `onItemClick`, say - marked `aria-selected` with a tint
|
|
203
|
+
* that stays when focus moves on. The page decides it: moving focus, a chevron, and opening or closing a row leave it
|
|
204
|
+
* where it is. Not on a `checkable` tree, whose rows are checked instead.
|
|
205
|
+
*/
|
|
206
|
+
selectedValue?: TreeValue | null;
|
|
207
|
+
/**
|
|
208
|
+
* Small tabs above the rows that narrow them, such as All actions · View only · Add and edit · Trash and delete on a
|
|
209
|
+
* permission tree. A filter's `match` is asked of every item without children (one without `match` shows all); the
|
|
210
|
+
* tree shows the leaves it takes and the parents that hold one. While it is on, checking a parent - or Select all -
|
|
211
|
+
* checks or unchecks only the leaves shown, a parent's box and count read only them, and a leaf the filter hides keeps
|
|
212
|
+
* its check exactly as it was. The value is the same whatever tab is on: switching tabs never changes it, and never
|
|
213
|
+
* makes a form dirty. The first filter is on at the start, unless `defaultFilter` names another.
|
|
214
|
+
*/
|
|
215
|
+
filters?: readonly TreeFilter<TItem>[];
|
|
216
|
+
/** With `filters`: the id of the filter on at the start. Default the first. */
|
|
217
|
+
defaultFilter?: string;
|
|
218
|
+
/**
|
|
219
|
+
* With `filters`: keeps the tab chosen in this browser, per `key` and `viewer` (the signed-in person), and opens it
|
|
220
|
+
* next time; nothing is kept without a viewer. Read after the first draw, as every kept choice is.
|
|
221
|
+
*/
|
|
222
|
+
rememberFilter?: {
|
|
223
|
+
key: string;
|
|
224
|
+
viewer?: string | null;
|
|
225
|
+
};
|
|
226
|
+
/** With `filters`: after a tab is chosen, its id. */
|
|
227
|
+
onFilterChange?: (id: string) => void;
|
|
228
|
+
/** With `filters`: what a screen reader calls the tabs. Default "Show". */
|
|
229
|
+
filtersLabel?: string;
|
|
162
230
|
/** The longest name the editor takes. Default 100. */
|
|
163
231
|
maxLabelLength?: number;
|
|
164
232
|
/** A click on a row without a checkbox, or Enter on it. Without it, a click expands or collapses a parent. */
|
|
@@ -173,6 +241,12 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
|
|
|
173
241
|
height?: number | string;
|
|
174
242
|
/** The tallest the rows' area grows before it scrolls. Default 420. */
|
|
175
243
|
maxHeight?: number | string;
|
|
244
|
+
/**
|
|
245
|
+
* Takes the height it is given, as a data table's `fill` does: the toolbar stays on top and the rows scroll inside
|
|
246
|
+
* under it, so the page around it never has to. `height` and `maxHeight` are ignored while it is on. Give it a cell
|
|
247
|
+
* that has a height: a side of an `AdminSplit` on an `AdminPage` with `fill` (nexus-admin), or a parent of your own.
|
|
248
|
+
*/
|
|
249
|
+
fill?: boolean;
|
|
176
250
|
/** Can be browsed, expanded, and focused, but not changed. A lock follows the label, and a try shows a note. */
|
|
177
251
|
readOnly?: boolean;
|
|
178
252
|
/** What a read-only tree says when someone tries to change it. Default "Read-only: this tree cannot be changed." */
|