nexus-shared 2.0.0 → 3.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 +446 -0
- package/LICENSE +21 -21
- package/README.md +1084 -903
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +96 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +44 -28
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +12 -8
- package/dist/Components/Documents/TabButtons.d.ts +29 -8
- package/dist/Components/Documents/TabButtons.js +24 -5
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +342 -71
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +58 -0
- package/dist/Components/Forms/ExcelImport.js +319 -0
- package/dist/Components/Forms/Form.js +9 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +55 -6
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +117 -20
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +2 -1
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +39 -18
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +262 -60
- package/dist/Components/Viewers/DataTableColumns.d.ts +32 -0
- package/dist/Components/Viewers/DataTableColumns.js +200 -0
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +8 -4
- package/dist/Components/Viewers/DataTableParts.js +24 -12
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +54 -9
- package/dist/Helpers/CrudBackend.js +103 -6
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +296 -0
- package/dist/Helpers/ExcelHelpers.d.ts +126 -0
- package/dist/Helpers/ExcelHelpers.js +344 -0
- package/dist/Helpers/FormStore.d.ts +11 -5
- package/dist/Helpers/FormStore.js +44 -6
- package/dist/Helpers/MessageBuilder.js +1 -2
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +46 -9
- package/dist/Helpers/PopoverHelpers.js +163 -24
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +14 -3
- package/dist/Helpers/RowsStore.js +37 -3
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +15 -4
- package/dist/Helpers/TableHelpers.js +28 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +92 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +227 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +85 -7
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +116 -9
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +48 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +10 -0
- package/dist/Shared.Index.js +10 -0
- package/package.json +85 -82
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -151
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -0
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -84
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -0
- package/src/Styles/Nexus.Form.css +279 -298
- package/src/Styles/Nexus.Index.css +37 -34
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -249
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -566
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -364
- package/src/Styles/Nexus.Table.css +1386 -1014
- package/src/Styles/Nexus.Theme.Picker.css +286 -159
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
|
@@ -13,20 +13,29 @@ function ProblemList({ form, names }) {
|
|
|
13
13
|
const { shown, more } = namesToShow(names);
|
|
14
14
|
return (_jsxs(_Fragment, { children: [names.length === 1 ? "1 field needs attention: " : `${names.length} fields need attention: `, shown.map((name, index) => (_jsxs("span", { children: [index > 0 && (index === shown.length - 1 && !more ? (shown.length > 2 ? ", and " : " and ") : ", "), _jsx("button", { type: "button", className: "nx-form-status__jump", onClick: () => form.focus(name), children: formInputLabel(form.inputs.find(params => params.name === name), name) })] }, name))), more > 0 && `, and ${more} more`, "."] }));
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* One line of what the form is doing: loading, saving, what needs attention, unsaved changes, or when it saved. In
|
|
18
|
+
* `errors` mode only an error that belongs to no field: each field already shows its own, all of them in view.
|
|
19
|
+
*/
|
|
20
|
+
function FormStatusLine({ form, status, mode }) {
|
|
18
21
|
const preferences = useDatePreferences();
|
|
19
22
|
let tone = "muted";
|
|
20
23
|
let content = null;
|
|
21
|
-
|
|
24
|
+
const errorsOnly = mode === "errors";
|
|
25
|
+
const names = errorsOnly ? [] : status.problems;
|
|
26
|
+
const problems = Boolean(status.message || names.length);
|
|
27
|
+
if (errorsOnly && !problems) {
|
|
28
|
+
// Nothing went wrong outside the fields, and nothing else is this form's to say.
|
|
29
|
+
}
|
|
30
|
+
else if (status.loading) {
|
|
22
31
|
content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }), _jsx("span", { children: "Loading\u2026" })] }));
|
|
23
32
|
}
|
|
24
33
|
else if (status.submitting) {
|
|
25
34
|
content = _jsx("span", { children: "Saving\u2026" });
|
|
26
35
|
}
|
|
27
|
-
else if (
|
|
36
|
+
else if (problems) {
|
|
28
37
|
tone = "error";
|
|
29
|
-
content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsxs("span", { children: [status.message, status.message &&
|
|
38
|
+
content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsxs("span", { children: [status.message, status.message && names.length > 0 && " ", names.length > 0 && _jsx(ProblemList, { form: form, names: names })] })] }));
|
|
30
39
|
}
|
|
31
40
|
else if (status.changed) {
|
|
32
41
|
content = _jsx("span", { children: status.draftSavedAt ? "Unsaved changes, kept on this device." : "Unsaved changes." });
|
|
@@ -35,7 +44,7 @@ function FormStatusLine({ form, status }) {
|
|
|
35
44
|
tone = "success";
|
|
36
45
|
content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconCircleCheck }), _jsxs("span", { children: ["Saved at ", formatFormTime(status.savedAt, preferences), "."] })] }));
|
|
37
46
|
}
|
|
38
|
-
return (_jsx("p", { className: cx("nx-form-status", `nx-form-status--${tone}
|
|
47
|
+
return (_jsx("p", { className: cx("nx-form-status", `nx-form-status--${tone}`, errorsOnly && "nx-form-status--errors"), "aria-live": "polite", children: content }));
|
|
39
48
|
}
|
|
40
49
|
/**
|
|
41
50
|
* Asks "Discard your changes?" when the form has changes. Resolves `true` to go on (nothing changed, or the user chose
|
|
@@ -63,10 +72,12 @@ export async function confirmDiscard(form) {
|
|
|
63
72
|
* <ApiForm form={form} buttons={false} inputs={inputs} api={api} />
|
|
64
73
|
* </Popup>
|
|
65
74
|
*/
|
|
66
|
-
export function FormButtons({ form, submitLabel = "Save", submitIcon, onCancel, confirmCancel = true, cancelLabel = "Cancel", resettable = false, resetLabel = "Reset", align = "end", size = "md", readOnly = false, className, }) {
|
|
75
|
+
export function FormButtons({ form, submitLabel = "Save", submitIcon, onCancel, confirmCancel = true, cancelLabel = "Cancel", resettable = false, resetLabel = "Reset", align = "end", size = "md", readOnly = false, statusLine = "full", block = false, loadingLabel = "Loading…", className, }) {
|
|
67
76
|
const store = form;
|
|
68
77
|
const status = useFormStatus(store);
|
|
69
78
|
useEffect(() => store.addButtons(), [store]);
|
|
79
|
+
// The quiet line says nothing while the values load, so the button does: same place, same size, no jump after.
|
|
80
|
+
const loadingInButton = statusLine === "errors" && status.loading;
|
|
70
81
|
async function cancel() {
|
|
71
82
|
if (confirmCancel && !(await confirmDiscard(store)))
|
|
72
83
|
return;
|
|
@@ -77,13 +88,13 @@ export function FormButtons({ form, submitLabel = "Save", submitIcon, onCancel,
|
|
|
77
88
|
const actions = [
|
|
78
89
|
resettable && !readOnly && (_jsx(Button, { size: buttonSize, variant: "ghost", icon: _jsx(InlineIcon, { icon: iconRefresh }), disabled: !status.changed || status.submitting || status.loading, onClick: () => store.reset(), children: resetLabel }, "reset")),
|
|
79
90
|
onCancel && (_jsx(Button, { size: buttonSize, onClick: () => void cancel(), children: cancelLabel }, "cancel")),
|
|
80
|
-
!readOnly && (_jsx(Button, { type: "submit", form: status.formId ?? undefined, size: buttonSize, variant: "primary", loading: status.submitting, disabled: status.loading, icon: submitIcon === undefined ? _jsx(InlineIcon, { icon: iconDeviceFloppy }) : (submitIcon ?? undefined), children: submitLabel }, "submit")),
|
|
91
|
+
!readOnly && (_jsx(Button, { type: "submit", form: status.formId ?? undefined, size: buttonSize, variant: "primary", loading: status.submitting || loadingInButton, disabled: status.loading, icon: submitIcon === undefined ? _jsx(InlineIcon, { icon: iconDeviceFloppy }) : (submitIcon ?? undefined), children: loadingInButton ? loadingLabel : submitLabel }, "submit")),
|
|
81
92
|
].filter(Boolean);
|
|
82
93
|
if (align === "start")
|
|
83
94
|
actions.reverse();
|
|
84
|
-
const line = _jsx(FormStatusLine, { form: store, status: status }, "status");
|
|
95
|
+
const line = _jsx(FormStatusLine, { form: store, status: status, mode: statusLine }, "status");
|
|
85
96
|
const bar = actions.length ? (_jsx("div", { className: "nx-form-buttons__actions", children: actions }, "actions")) : null;
|
|
86
|
-
return _jsx("div", { className: cx("nx-form-buttons", `nx-form-buttons--${align}`, className), children: align === "start" ? [bar, line] : [line, bar] });
|
|
97
|
+
return _jsx("div", { className: cx("nx-form-buttons", `nx-form-buttons--${align}`, block && "nx-form-buttons--block", className), children: align === "start" ? [bar, line] : [line, bar] });
|
|
87
98
|
}
|
|
88
99
|
/**
|
|
89
100
|
* A `Form` that submits: every value is checked on Save (the inputs' own rules, the form's `validate`, rows not on the
|
|
@@ -104,7 +115,7 @@ export function FormButtons({ form, submitLabel = "Save", submitIcon, onCancel,
|
|
|
104
115
|
* />
|
|
105
116
|
*/
|
|
106
117
|
export function SubmitForm(props) {
|
|
107
|
-
const { onSubmit: _onSubmit, onSaved: _onSaved, onCancel, confirmCancel, submitLabel, submitIcon, cancelLabel, resettable, resetLabel, resetOnSuccess: _resetOnSuccess, buttons = "end", confirmLeave, id, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...formProps } = props;
|
|
118
|
+
const { onSubmit: _onSubmit, onSaved: _onSaved, onCancel, confirmCancel, submitLabel, submitIcon, cancelLabel, resettable, resetLabel, resetOnSuccess: _resetOnSuccess, buttons = "end", statusLine, confirmLeave, id, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...formProps } = props;
|
|
108
119
|
const store = useFormStore(props.form);
|
|
109
120
|
const formRef = useRef(null);
|
|
110
121
|
const generatedId = useId();
|
|
@@ -194,5 +205,5 @@ export function SubmitForm(props) {
|
|
|
194
205
|
return (_jsxs("form", { ref: formRef, id: formId, className: cx("nx-submit-form", className), style: style, noValidate: true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, onSubmit: event => {
|
|
195
206
|
event.preventDefault();
|
|
196
207
|
void submit();
|
|
197
|
-
}, onKeyDown: onKeyDown, children: [_jsx(Form, { ...formProps, form: store }), buttons !== false && (_jsx(FormButtons, { form: store, align: buttons, submitLabel: submitLabel, submitIcon: submitIcon, onCancel: onCancel, confirmCancel: confirmCancel, cancelLabel: cancelLabel, resettable: resettable, resetLabel: resetLabel, size: formProps.size, readOnly: locked }))] }));
|
|
208
|
+
}, onKeyDown: onKeyDown, children: [_jsx(Form, { ...formProps, form: store }), buttons !== false && (_jsx(FormButtons, { form: store, align: buttons, submitLabel: submitLabel, submitIcon: submitIcon, onCancel: onCancel, confirmCancel: confirmCancel, cancelLabel: cancelLabel, resettable: resettable, resetLabel: resetLabel, size: formProps.size, readOnly: locked, statusLine: statusLine }))] }));
|
|
198
209
|
}
|
|
@@ -188,7 +188,8 @@ export function DateTimePicker(props) {
|
|
|
188
188
|
const cap = natural > room ? `${Math.max(0, room)}px` : "";
|
|
189
189
|
if (panel.style.maxHeight !== cap)
|
|
190
190
|
panel.style.maxHeight = cap;
|
|
191
|
-
|
|
191
|
+
// No arrow: the panel opens flush under the box and is read as the field's own calendar.
|
|
192
|
+
placeUnderAnchor(panel, anchor, { gap: PANEL_GAP, margin: PANEL_MARGIN, arrow: false });
|
|
192
193
|
};
|
|
193
194
|
place();
|
|
194
195
|
if (focusPanelRef.current) {
|
|
@@ -11,7 +11,7 @@ export declare const DROPDOWN_STATE_LABELS: Record<DropdownValueState, string>;
|
|
|
11
11
|
/** The tooltip of a current value that cannot be picked again. */
|
|
12
12
|
export declare const DROPDOWN_STATE_NOTES: Record<DropdownValueState, string>;
|
|
13
13
|
type AnyColumn = DropdownColumn<never>;
|
|
14
|
-
/**
|
|
14
|
+
/** The shared `Avatar`, sized to the dropdown's pictures by its class. */
|
|
15
15
|
export declare function DropdownAvatar({ src, name }: {
|
|
16
16
|
src: unknown;
|
|
17
17
|
name: string;
|
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { Icon, iconCheck, iconX, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { isValidElement, memo, useState } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isMediaColumn, readField } from "../../Helpers/DropdownHelpers.js";
|
|
7
|
+
import { Avatar } from "../Viewers/Avatar.js";
|
|
7
8
|
export const DROPDOWN_STATE_LABELS = {
|
|
8
9
|
inactive: "Inactive",
|
|
9
10
|
unavailable: "Unavailable",
|
|
@@ -15,12 +16,9 @@ export const DROPDOWN_STATE_NOTES = {
|
|
|
15
16
|
unavailable: "No longer in the list: kept as the current value. Once removed, it cannot be chosen again.",
|
|
16
17
|
unknown: "Not found in the list: kept as the current value. Once removed, it cannot be chosen again.",
|
|
17
18
|
};
|
|
18
|
-
/**
|
|
19
|
+
/** The shared `Avatar`, sized to the dropdown's pictures by its class. */
|
|
19
20
|
export function DropdownAvatar({ src, name }) {
|
|
20
|
-
|
|
21
|
-
const [failed, setFailed] = useState(null);
|
|
22
|
-
const photo = url !== "" && failed !== url;
|
|
23
|
-
return (_jsx("span", { className: "nx-dropdown-avatar", "data-tone": photo ? undefined : avatarTone(name), "aria-hidden": "true", children: photo ? _jsx("img", { src: url, alt: "", loading: "lazy", decoding: "async", onError: () => setFailed(url) }) : initials(name) }));
|
|
21
|
+
return _jsx(Avatar, { name: name, src: src, className: "nx-dropdown-avatar" });
|
|
24
22
|
}
|
|
25
23
|
/** A small 4:3 picture, such as a flag or a logo; an empty frame when there is none or it fails to load. */
|
|
26
24
|
export function DropdownImage({ src }) {
|
|
@@ -64,7 +64,7 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
|
|
|
64
64
|
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
65
65
|
}
|
|
66
66
|
const heading = (_jsxs(_Fragment, { children: [_jsx("span", { id: titleId, className: "nx-group-form__title", children: title }), tags.map(tag => (_jsx("span", { className: "nx-group-form__tag", children: tag }, tag))), summary && _jsx("span", { className: "nx-group-form__summary", children: summary }), problems > 0 && (_jsxs("span", { className: "nx-group-form__problems", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), problems, " to fix"] }))] }));
|
|
67
|
-
return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell", CHOICE_TYPES.has(params.type) && "nx-group-form__cell--choice"), style: { "--nx-span": Math.max(1, Math.min(12, params.span ?? 12)) }, "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: params, index: index, error: store.shownError(rowKey, params.name), layout: "group", idPrefix: context.idPrefix, size: context.size, design: context.design, readOnly: context.readOnly, disabled: context.disabled }, store.revision(rowKey, params.name)) }, params.name))) }))] }));
|
|
67
|
+
return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell", CHOICE_TYPES.has(params.type) && "nx-group-form__cell--choice"), style: { "--nx-span": Math.max(1, Math.min(12, params.span ?? 12)) }, "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: "group", idPrefix: context.idPrefix, size: context.size, design: context.design, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))) }))] }));
|
|
68
68
|
});
|
|
69
69
|
/**
|
|
70
70
|
* The rows of a `TabularForm` as sections: each row's inputs with their own labels, on a 12-column grid (`span` in
|
|
@@ -153,6 +153,8 @@ export function GroupForm(props) {
|
|
|
153
153
|
idPrefix: frame.id,
|
|
154
154
|
size,
|
|
155
155
|
design,
|
|
156
|
+
cellLock: rows.cellLock,
|
|
157
|
+
cellParams: rows.cellParams,
|
|
156
158
|
readOnly,
|
|
157
159
|
disabled,
|
|
158
160
|
removable,
|
|
@@ -164,7 +166,7 @@ export function GroupForm(props) {
|
|
|
164
166
|
preferences,
|
|
165
167
|
sectionTitle,
|
|
166
168
|
summary,
|
|
167
|
-
}), [frame.id, size, design, readOnly, disabled, removable, sortable, rows.canAdd, collapsible, defaultCollapsed, snapshot.showAll, preferences, sectionTitle, summary]);
|
|
169
|
+
}), [frame.id, size, design, rows.cellLock, rows.cellParams, readOnly, disabled, removable, sortable, rows.canAdd, collapsible, defaultCollapsed, snapshot.showAll, preferences, sectionTitle, summary]);
|
|
168
170
|
const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
|
|
169
171
|
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-group-form", `nx-group-form--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || 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 }), count === 0 ? (_jsx("p", { className: "nx-group-form__empty", children: frame.emptyMessage })) : (_jsx("ol", { ref: listRef, className: "nx-group-form__list", children: snapshot.keys.map((key, index) => (_jsx(GroupSection, { store: store, rowKey: key, index: index, count: count, inputs: inputs, context: context, isNew: !loadedKeys.has(key), onAction: onAction }, `${snapshot.generation}:${key}`))) })), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
170
172
|
}
|
|
@@ -21,6 +21,8 @@ export interface InputOverrides {
|
|
|
21
21
|
design?: InputDesign;
|
|
22
22
|
/** On top of the params: `true` makes the input read-only even when its params do not. */
|
|
23
23
|
readOnly?: boolean;
|
|
24
|
+
/** Replaces the params' own read-only note, for a cell locked by `cellReadOnly` rather than by its params. */
|
|
25
|
+
readOnlyMessage?: string;
|
|
24
26
|
/** On top of the params, like `readOnly`. */
|
|
25
27
|
disabled?: boolean;
|
|
26
28
|
fieldName?: string;
|
|
@@ -25,7 +25,7 @@ import { TreeView } from "./TreeView.js";
|
|
|
25
25
|
export function renderInput(params, overrides = {}) {
|
|
26
26
|
if (params.hidden)
|
|
27
27
|
return null;
|
|
28
|
-
const { type, span: _span, width: _width, grow: _grow, hidden: _hidden, onValueChange: ownChange, ...rest } = params;
|
|
28
|
+
const { type, section: _section, span: _span, width: _width, grow: _grow, hidden: _hidden, dependsOn: _dependsOn, onValueChange: ownChange, ...rest } = params;
|
|
29
29
|
const { defaultValue, onValueChange, readOnly, disabled, ...set } = overrides;
|
|
30
30
|
const props = { ...rest, ...set };
|
|
31
31
|
if (readOnly)
|
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { InlineIcon, iconMinus, iconPlus } from "nexus-icons";
|
|
4
4
|
import { useEffect, useLayoutEffect, useRef, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
-
import { clampNumber, cleanNumberText,
|
|
6
|
+
import { clampNumber, cleanNumberText, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
|
|
7
|
+
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
7
8
|
import { validateNumber } from "../../Helpers/ValidationHelpers.js";
|
|
8
9
|
import { Field, hasContent } from "./Field.js";
|
|
9
10
|
import { setInputValue, useInputField } from "./InputField.js";
|
|
@@ -23,7 +24,10 @@ const isNumber = (value) => value !== null && !Number.isNaN(value);
|
|
|
23
24
|
* @example <NumberBox label="Amount" prefix="Rs" min={0} decimals={2} fixedDecimals grouping required />
|
|
24
25
|
*/
|
|
25
26
|
export function NumberBox(props) {
|
|
26
|
-
const { label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale
|
|
27
|
+
const { label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale: ownLocale, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onPaste, onClick, ...inputProps } = props;
|
|
28
|
+
// Without a locale of its own, the box writes and reads numbers the way the person chose (Number scale, Thousands).
|
|
29
|
+
const preferred = useNumberLocale();
|
|
30
|
+
const locale = ownLocale ?? preferred;
|
|
27
31
|
const format = { decimals, fixedDecimals, grouping, locale };
|
|
28
32
|
const { decimal, group } = getNumberSymbols(locale);
|
|
29
33
|
const textRules = { decimals, negative: min === undefined || min < 0 };
|
|
@@ -55,8 +59,11 @@ export function NumberBox(props) {
|
|
|
55
59
|
const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, {
|
|
56
60
|
keys: ["ArrowUp", "ArrowDown", "PageUp", "PageDown"],
|
|
57
61
|
});
|
|
62
|
+
// The number the box holds, as last read in the marks it was written in.
|
|
63
|
+
const numberRef = useRef(initial);
|
|
58
64
|
/** Writes what depends on the value straight to the DOM, so typing re-renders nothing: the hidden input and the button limits. */
|
|
59
65
|
function sync(number) {
|
|
66
|
+
numberRef.current = number;
|
|
60
67
|
if (hiddenRef.current)
|
|
61
68
|
hiddenRef.current.value = isNumber(number) ? String(number) : "";
|
|
62
69
|
for (const [button, direction] of [
|
|
@@ -69,6 +76,21 @@ export function NumberBox(props) {
|
|
|
69
76
|
button?.removeAttribute("aria-disabled");
|
|
70
77
|
}
|
|
71
78
|
}
|
|
79
|
+
// The person changed how numbers are written (Number scale, Thousands): the number the box held is written again in
|
|
80
|
+
// the new marks. Its text is not read again, because by now it may be either: React has already rewritten an untouched
|
|
81
|
+
// box's default text, and "1,250.5" read with a decimal comma would be 1.2505. Runs before the effects below read it.
|
|
82
|
+
const localeRef = useRef(locale);
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
const previous = localeRef.current;
|
|
85
|
+
localeRef.current = locale;
|
|
86
|
+
const input = field.elementRef.current;
|
|
87
|
+
const number = numberRef.current;
|
|
88
|
+
if (previous === locale || !input || !input.value || !isNumber(number))
|
|
89
|
+
return;
|
|
90
|
+
input.value = document.activeElement === input ? editText(number) : displayText(number);
|
|
91
|
+
sync(number);
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [locale]);
|
|
72
94
|
// A controlled value that differs from the box's text replaces it. Text that already means this number ("12.") stays.
|
|
73
95
|
useLayoutEffect(() => {
|
|
74
96
|
const input = field.elementRef.current;
|
|
@@ -88,7 +88,8 @@ function ReadOnlyNote({ ref, message }) {
|
|
|
88
88
|
return;
|
|
89
89
|
if (!isPopoverOpen(note))
|
|
90
90
|
note.showPopover?.();
|
|
91
|
-
|
|
91
|
+
// No arrow: the note is not a panel, and it answers the whole box rather than one button in it.
|
|
92
|
+
const place = () => placeUnderAnchor(note, shown.anchor.getBoundingClientRect(), { align: shown.align, arrow: false });
|
|
92
93
|
place();
|
|
93
94
|
return followAnchor(place);
|
|
94
95
|
}, [shown]);
|
|
@@ -34,7 +34,16 @@ export declare function useRowsInput<TRow extends RowValue>(props: RowsInputProp
|
|
|
34
34
|
move(key: string, delta: number): void;
|
|
35
35
|
undo(): void;
|
|
36
36
|
};
|
|
37
|
+
cellLock: (key: string, name: string, index: number) => string | boolean | null | undefined;
|
|
38
|
+
rowLock: (key: string | null, index: number) => string | boolean | null | undefined;
|
|
39
|
+
cellParams: (key: string, params: InputParams, index: number) => InputParams;
|
|
37
40
|
};
|
|
41
|
+
/** What a layout asks for each cell: the reason it is generated, `true` for no reason, or nothing. */
|
|
42
|
+
export type CellLock = (key: string, name: string, index: number) => string | boolean | null | undefined;
|
|
43
|
+
/** What a layout asks for each cell: its own params, which are the column's unless `cellParams` answers for its row. */
|
|
44
|
+
export type CellParams = (key: string, params: InputParams, index: number) => InputParams;
|
|
45
|
+
/** What a layout asks for each row: the reason nobody may change it, `true` for no reason, or nothing. From `rowReadOnly`. */
|
|
46
|
+
export type RowLock = (key: string | null, index: number) => string | boolean | null | undefined;
|
|
38
47
|
/** The first control of an element that takes focus. */
|
|
39
48
|
export declare function firstFocusable(element: Element | null): HTMLElement | null;
|
|
40
49
|
/** Subscribes a row's component to its row: it renders again only when the row's messages or code-set values change. */
|
|
@@ -55,13 +64,15 @@ export interface RowCellProps {
|
|
|
55
64
|
design?: InputDesign;
|
|
56
65
|
readOnly: boolean;
|
|
57
66
|
disabled: boolean;
|
|
67
|
+
/** This one cell holds a generated value: the reason, `true` for no reason, or nothing. From `cellReadOnly`. */
|
|
68
|
+
lock?: string | boolean | null;
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
60
71
|
* One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
|
|
61
72
|
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
62
73
|
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
63
74
|
*/
|
|
64
|
-
export declare const RowCell: import("react").MemoExoticComponent<({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled }: RowCellProps) => ReactNode>;
|
|
75
|
+
export declare const RowCell: import("react").MemoExoticComponent<({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }: RowCellProps) => ReactNode>;
|
|
65
76
|
/** The legend of the rows, with the required mark and, when read-only, a lock. */
|
|
66
77
|
export declare function RowsLegend({ id, label, required, lock }: {
|
|
67
78
|
id: string;
|
|
@@ -26,9 +26,9 @@ function sameRows(a, b) {
|
|
|
26
26
|
* listener, a parent form's data and reset, and the handle given to `ref`.
|
|
27
27
|
*/
|
|
28
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;
|
|
29
|
+
const { inputs, required, min, max, unique, validate, rowKey, newRow, onRowChange, deriveRows, itemLabel, itemLabelPlural, value, defaultValue, name, readOnly = false, disabled = false, ref } = props;
|
|
30
30
|
const preferences = useDatePreferences();
|
|
31
|
-
const options = { inputs, required, min, max, unique, validate, rowKey, newRow, onRowChange, itemLabel, itemLabelPlural, fallbackItem, context: { preferences } };
|
|
31
|
+
const options = { inputs, required, min, max, unique, validate, rowKey, newRow, onRowChange, deriveRows, itemLabel, itemLabelPlural, fallbackItem, context: { preferences } };
|
|
32
32
|
const [store] = useState(() => new RowsStore(options, value ?? defaultValue ?? []));
|
|
33
33
|
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
34
34
|
const proxyRef = useRef(null);
|
|
@@ -135,10 +135,10 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
135
135
|
add: (values, index) => {
|
|
136
136
|
actions.add(values, index);
|
|
137
137
|
},
|
|
138
|
-
remove: index => {
|
|
138
|
+
remove: (index, options) => {
|
|
139
139
|
const key = store.keyAt(index);
|
|
140
140
|
if (key)
|
|
141
|
-
store.remove(key);
|
|
141
|
+
store.remove(key, options);
|
|
142
142
|
},
|
|
143
143
|
validate: () => {
|
|
144
144
|
const place = store.validateAll();
|
|
@@ -160,7 +160,49 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
160
160
|
}),
|
|
161
161
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
162
|
[store]);
|
|
163
|
-
|
|
163
|
+
// Stable, so a sheet of thousands of rows does not render again because the parent handed a new function.
|
|
164
|
+
const rowLock = useCallback((key, index) => {
|
|
165
|
+
const ask = latest.current.props.rowReadOnly;
|
|
166
|
+
if (!ask || !key)
|
|
167
|
+
return undefined;
|
|
168
|
+
const row = store.row(key);
|
|
169
|
+
return row ? ask(row, index) : undefined;
|
|
170
|
+
}, [store]);
|
|
171
|
+
const cellLock = useCallback((key, name, index) => {
|
|
172
|
+
// A row nobody may change locks every one of its cells, with the row's reason.
|
|
173
|
+
const whole = rowLock(key, index);
|
|
174
|
+
if (whole)
|
|
175
|
+
return whole;
|
|
176
|
+
const ask = latest.current.props.cellReadOnly;
|
|
177
|
+
if (!ask)
|
|
178
|
+
return undefined;
|
|
179
|
+
const row = store.row(key);
|
|
180
|
+
return row ? ask(row, name, index) : undefined;
|
|
181
|
+
}, [store, rowLock]);
|
|
182
|
+
// A cell's own params, merged once per answer: the same answer gives the same object, so the cell does not draw again.
|
|
183
|
+
const merged = useRef(new WeakMap());
|
|
184
|
+
const cellParams = useCallback((key, params, index) => {
|
|
185
|
+
const ask = latest.current.props.cellParams;
|
|
186
|
+
if (!ask)
|
|
187
|
+
return params;
|
|
188
|
+
const row = store.row(key);
|
|
189
|
+
const own = row ? ask(row, params.name, index) : null;
|
|
190
|
+
if (!own)
|
|
191
|
+
return params;
|
|
192
|
+
let byColumn = merged.current.get(own);
|
|
193
|
+
if (!byColumn)
|
|
194
|
+
merged.current.set(own, (byColumn = new WeakMap()));
|
|
195
|
+
let result = byColumn.get(params);
|
|
196
|
+
if (!result) {
|
|
197
|
+
// Descriptors rather than a spread, so a getter on either side is still read as the cell draws, as it would be
|
|
198
|
+
// on the column's own params; the column keeps its type and its name whatever the answer holds.
|
|
199
|
+
const { type: _type, name: _name, ...rest } = Object.getOwnPropertyDescriptors(own);
|
|
200
|
+
result = Object.defineProperties({}, { ...Object.getOwnPropertyDescriptors(params), ...rest });
|
|
201
|
+
byColumn.set(params, result);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}, [store]);
|
|
205
|
+
return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams };
|
|
164
206
|
}
|
|
165
207
|
function addButtonOf(proxy) {
|
|
166
208
|
return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__add:not(:disabled)") ?? null;
|
|
@@ -191,7 +233,7 @@ export function labelText(params) {
|
|
|
191
233
|
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
192
234
|
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
193
235
|
*/
|
|
194
|
-
export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled }) {
|
|
236
|
+
export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }) {
|
|
195
237
|
const [initial] = useState(() => store.get(rowKey, params.name));
|
|
196
238
|
const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
|
|
197
239
|
const needsName = params.type === "radio-group" || params.type === "checkbox-group";
|
|
@@ -205,6 +247,13 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
|
|
|
205
247
|
readOnly,
|
|
206
248
|
disabled,
|
|
207
249
|
};
|
|
250
|
+
// A generated cell: read-only however its params have it, and saying why, since a box that ignores typing without a
|
|
251
|
+
// word reads as broken. Code still writes it — that is what makes it generated rather than merely locked.
|
|
252
|
+
if (lock) {
|
|
253
|
+
overrides.readOnly = true;
|
|
254
|
+
if (typeof lock === "string")
|
|
255
|
+
overrides.readOnlyMessage = lock;
|
|
256
|
+
}
|
|
208
257
|
if (error !== undefined)
|
|
209
258
|
overrides.error = error;
|
|
210
259
|
// A required radio means one row has it on, which the rows check; each radio alone is not required.
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Fragment, useEffect, useEffectEvent, useRef, } from "react";
|
|
4
4
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
5
|
import { clampNumber, countDecimals, formatNumber, roundDecimal } from "../../Helpers/NumberHelpers.js";
|
|
6
|
+
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
6
7
|
import { GroupFooter, leavesField, OPTION_ATTRIBUTE, useChoiceField } from "./ChoiceField.js";
|
|
7
8
|
import { hasContent } from "./Field.js";
|
|
8
9
|
import { setInputValue } from "./InputField.js";
|
|
@@ -54,7 +55,9 @@ function SliderField(props) {
|
|
|
54
55
|
const { readOnlyMessage = range ? "Read-only: this range cannot be changed." : "Read-only: this value cannot be changed." } = props;
|
|
55
56
|
const span = max - min || 1;
|
|
56
57
|
const initial = (values ?? defaultValues).map(value => clampNumber(value, min, max));
|
|
57
|
-
|
|
58
|
+
// Without a locale of its own, the values are written the way the person chose.
|
|
59
|
+
const numbers = useNumberLocale();
|
|
60
|
+
const text = (value) => (format ? format(value) : `${prefix ?? ""}${formatNumber(value, { decimals, fixedDecimals, grouping, locale: locale ?? numbers })}${suffix ?? ""}`);
|
|
58
61
|
const fraction = (value) => (clampNumber(value, min, max) - min) / span;
|
|
59
62
|
const controlRef = useRef(null);
|
|
60
63
|
const valueRef = useRef(null);
|
|
@@ -5,9 +5,13 @@ import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces
|
|
|
5
5
|
* come as params, the same objects a form takes, so the same list can show as sections in a `GroupForm`.
|
|
6
6
|
*
|
|
7
7
|
* Only the rows in view are on the page, so a sheet of thousands of rows opens and types as fast as a short one; the
|
|
8
|
-
* values live in a store outside React, and typing renders nothing. The header
|
|
9
|
-
*
|
|
10
|
-
* a text or number cell moves down the column, and on the last row adds a row.
|
|
8
|
+
* values live in a store outside React, and typing renders nothing. The header and the numbered column stay in place
|
|
9
|
+
* while the table scrolls. Each row has one control, beside its number and never in its place: a menu of insert,
|
|
10
|
+
* duplicate, move and remove. Enter in a text or number cell moves down the column, and on the last row adds a row.
|
|
11
|
+
*
|
|
12
|
+
* Every row is one line high, whatever it holds, so nothing shifts as the table is used; a column generated for every
|
|
13
|
+
* row (`cellReadOnly`) says so once on its header rather than on each of its cells; and `maxHeight` is brought down
|
|
14
|
+
* to whole rows, because a scroller that cuts the last one in half reads as broken rather than as scrollable.
|
|
11
15
|
*
|
|
12
16
|
* Inside a `<form>`, the rows are sent as JSON under `name`; a row with a problem blocks submit, and the first one
|
|
13
17
|
* scrolls into view with focus.
|