nexus-shared 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- 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/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -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 +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -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 +195 -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/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- 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 +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -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 +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- 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 +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- 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/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -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/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -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 +78 -0
- package/dist/Helpers/FileAdapter.js +102 -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 +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -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 +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- 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 +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- 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 +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -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 +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconPencil, iconPhoto, iconTrash, iconUpload, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useId, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { getFileAdapter } from "../../Services/FileApi.js";
|
|
7
|
+
import { preloadImageEditor } from "../../Services/ImageEditorLoader.js";
|
|
8
|
+
import { Button } from "../Documents/Button.js";
|
|
9
|
+
import { busyProps, openPictureGallery, useFocusThroughChange, useOwnerFiles } from "../Viewers/OwnerFiles.js";
|
|
10
|
+
import { ChoiceLegend, GroupFooter } from "./ChoiceField.js";
|
|
11
|
+
const HEIGHTS = { sm: 72, md: 112, lg: 152 };
|
|
12
|
+
/**
|
|
13
|
+
* A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
|
|
14
|
+
* Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
|
|
15
|
+
* more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
|
|
16
|
+
* Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
|
|
17
|
+
* once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
|
|
18
|
+
* it and sends nothing under its name.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* inputs: (action, row) => [
|
|
22
|
+
* { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
|
|
23
|
+
* ...
|
|
24
|
+
* ]
|
|
25
|
+
*/
|
|
26
|
+
export function PictureInput({ label, hint, size = "md", disabled = false, readOnly = false, subject = "record", id, className, style, "aria-label": ariaLabel, ...params }) {
|
|
27
|
+
const generated = useId();
|
|
28
|
+
const fieldId = id ?? generated;
|
|
29
|
+
const [failed, setFailed] = useState(null);
|
|
30
|
+
const files = useOwnerFiles({ ...params, readOnly: readOnly || disabled });
|
|
31
|
+
const { status, data, busy, canWrite, readOnlyReason, editorProblem, actions } = files;
|
|
32
|
+
// A keyboard's focus stays in the input through a change: on the button pressed, or the first left when it went.
|
|
33
|
+
const rootRef = useRef(null);
|
|
34
|
+
useFocusThroughChange(rootRef, busy);
|
|
35
|
+
const primary = data?.files.find(file => file.isPrimary) ?? null;
|
|
36
|
+
const url = primary ? getFileAdapter().fileUrl(primary, "thumbnail") : null;
|
|
37
|
+
const shown = url && failed !== url;
|
|
38
|
+
const ratio = typeof params.aspect === "number" && params.aspect > 0 ? params.aspect : 1;
|
|
39
|
+
const preview = { "--nx-picture-h": `${HEIGHTS[size]}px`, "--nx-picture-ratio": ratio };
|
|
40
|
+
const unsaved = !params.ownerId;
|
|
41
|
+
// A category that keeps one file: a new picture takes its place, and Remove deletes it (there is nothing else to show).
|
|
42
|
+
const single = data?.category.maxCount === 1;
|
|
43
|
+
const kept = data?.files ?? [];
|
|
44
|
+
const note = unsaved
|
|
45
|
+
? `Save the ${subject} first, then add its picture.`
|
|
46
|
+
: status.kind === "failed"
|
|
47
|
+
? status.message
|
|
48
|
+
: busy
|
|
49
|
+
? `${busy}…`
|
|
50
|
+
: editorProblem
|
|
51
|
+
? // Why Upload or Edit did not open the editor, until a try opens it: never a button that does nothing.
|
|
52
|
+
editorProblem
|
|
53
|
+
: // Why it cannot be changed, when the page or the backend says (`lockReason`, a refused key): never a silent input.
|
|
54
|
+
(readOnlyReason ?? hint ?? (data && canWrite ? `${data.files.length} of ${data.category.maxCount} ${single ? "picture" : "pictures"} kept.` : undefined));
|
|
55
|
+
const problem = status.kind === "failed" || (!unsaved && !busy && Boolean(editorProblem));
|
|
56
|
+
const named = typeof label === "string" && label ? label : "Picture";
|
|
57
|
+
/** The pictures window, as All pictures opens it: what changes there is read again here. */
|
|
58
|
+
const openAll = () => openPictureGallery({
|
|
59
|
+
...params,
|
|
60
|
+
ownerId: params.ownerId,
|
|
61
|
+
readOnly: readOnly || disabled,
|
|
62
|
+
onChange: primary => {
|
|
63
|
+
void actions.reload();
|
|
64
|
+
params.onChange?.(primary);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const pictureClass = cx("nx-picture", params.shape === "circle" && "nx-picture--round", !shown && "nx-picture--empty", status.kind === "loading" && "nx-picture--loading");
|
|
68
|
+
const face = shown ? _jsx("img", { src: url, alt: "", decoding: "async", onError: () => setFailed(url) }) : _jsx(InlineIcon, { icon: iconPhoto });
|
|
69
|
+
return (_jsxs("fieldset", { ref: rootRef, id: fieldId, className: cx("nx-choices", size !== "md" && `nx-choices--${size}`, "nx-picture-input", className), style: style, disabled: disabled, "aria-label": ariaLabel, "aria-busy": status.kind === "loading" || Boolean(busy) || undefined, "aria-describedby": note ? `${fieldId}-message` : undefined, "data-readonly": readOnly || undefined, children: [_jsx(ChoiceLegend, { label: label }), _jsxs("div", { className: "nx-picture-input__body", children: [!unsaved && data ? (_jsx("button", { type: "button", className: pictureClass, style: preview, "aria-label": primary ? `${named}: open the pictures` : `${named}: none yet, open the pictures`, "aria-haspopup": "dialog", title: "All pictures", ...busyProps(busy, openAll), onPointerEnter: preloadImageEditor, children: face })) : (_jsx("span", { className: pictureClass, style: preview, role: "img", "aria-label": primary ? `${named}: ${primary.name}` : "No picture", children: face })), !unsaved && data && (_jsxs("div", { className: "nx-picture-input__actions", role: "group", children: [canWrite && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconUpload }), ...busyProps(busy, () => void actions.replace()), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: single && kept.length > 0 ? "Replace" : "Upload" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconPencil }), ...busyProps(busy, () => void actions.edit(primary)), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: "Edit" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), ...busyProps(busy, () => void (single ? actions.remove(primary) : actions.makePrimary(null))), children: single ? "Delete" : "Remove" })), (data.category.maxCount > 1 || kept.length > 1 || (kept.length > 0 && !primary)) && (_jsxs(Button, { size: "sm", variant: "ghost", ...busyProps(busy, openAll), children: ["All pictures (", data.files.length, ")"] }))] }))] }), _jsx(GroupFooter, { id: `${fieldId}-message`, hint: problem ? undefined : note, error: problem ? note : undefined })] }));
|
|
70
|
+
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { ChangeEvent } from "react";
|
|
2
|
-
import type { OptionValue } from "../../Interfaces/InputInterfaces.ts";
|
|
2
|
+
import type { ChoiceDesign, OptionValue } from "../../Interfaces/InputInterfaces.ts";
|
|
3
3
|
import { type ChoiceGroupProps } from "./ChoiceField.tsx";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How a `RadioGroup` draws its options: `default` (a circle beside each label), `cards` (a card each, with its
|
|
6
|
+
* description), or `tabs` (nav-style tabs on one track, as `TabButtons` draws them).
|
|
7
|
+
*/
|
|
8
|
+
export type RadioGroupDesign = ChoiceDesign | "tabs";
|
|
9
|
+
export interface RadioGroupProps<T extends OptionValue = string> extends Omit<ChoiceGroupProps<T>, "design"> {
|
|
10
|
+
/**
|
|
11
|
+
* `default`: a circle beside each label. `cards`: each option is a card. `tabs`: the options as nav-style tabs on one
|
|
12
|
+
* track (`TabButtons`' look), wrapping onto more rows where they do not fit; each option's `description` is its
|
|
13
|
+
* tooltip and what a screen reader hears with it, and the chosen option's shows under the track. `orientation` and
|
|
14
|
+
* `columns` do not apply to tabs.
|
|
15
|
+
*/
|
|
16
|
+
design?: RadioGroupDesign;
|
|
5
17
|
/** The chosen value, or `null` for none. With `value`, the group is controlled; pair it with `onValueChange`. */
|
|
6
18
|
value?: T | null;
|
|
7
19
|
defaultValue?: T | null;
|
|
@@ -12,7 +24,8 @@ export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceG
|
|
|
12
24
|
}
|
|
13
25
|
/**
|
|
14
26
|
* One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
|
|
15
|
-
* move the choice. `design="cards"` shows each option as a card with its description
|
|
27
|
+
* move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
|
|
28
|
+
* whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
|
|
16
29
|
* "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
|
|
17
30
|
*
|
|
18
31
|
* @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { validateSelection } from "../../Helpers/ValidationHelpers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
|
+
import { ChoiceItem, ChoiceLegend, ChoiceTab, ChoiceTabNotes, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, showChosenNote, useChoiceField } from "./ChoiceField.js";
|
|
5
6
|
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
6
7
|
/**
|
|
7
8
|
* One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
|
|
8
|
-
* move the choice. `design="cards"` shows each option as a card with its description
|
|
9
|
+
* move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
|
|
10
|
+
* whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
|
|
9
11
|
* "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
|
|
10
12
|
*
|
|
11
13
|
* @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
|
|
@@ -14,6 +16,7 @@ export function RadioGroup(props) {
|
|
|
14
16
|
const { options, name, label, hint, required, validate, value, defaultValue, readOnly = false, disabled, onValueChange } = props;
|
|
15
17
|
const { readOnlyMessage = "Read-only: this choice cannot be changed." } = props;
|
|
16
18
|
const { size = "md" } = props;
|
|
19
|
+
const tabs = props.design === "tabs";
|
|
17
20
|
const chosen = (inputs) => {
|
|
18
21
|
const input = inputs.find(item => item.checked);
|
|
19
22
|
return input ? [options[Number(input.getAttribute(OPTION_ATTRIBUTE))].value] : [];
|
|
@@ -21,6 +24,8 @@ export function RadioGroup(props) {
|
|
|
21
24
|
const field = useChoiceField(props, {
|
|
22
25
|
check: (inputs, fieldName) => validateSelection(chosen(inputs), { required, validate: validate && (values => validate(values[0] ?? null)) }, fieldName),
|
|
23
26
|
checkKeys: [value, required, validate, options],
|
|
27
|
+
// Tabs show the chosen option's description under the track: it follows every change and a form's reset.
|
|
28
|
+
sync: tabs ? showChosenNote : undefined,
|
|
24
29
|
});
|
|
25
30
|
function handleChange(event) {
|
|
26
31
|
if (readOnly)
|
|
@@ -39,5 +44,5 @@ export function RadioGroup(props) {
|
|
|
39
44
|
}
|
|
40
45
|
const invalid = Boolean(field.error);
|
|
41
46
|
const layout = groupLayout(props);
|
|
42
|
-
return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), _jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
|
|
47
|
+
return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), tabs ? (_jsx("div", { className: cx("nx-tabs", `nx-tabs--${size}`, "nx-tabs--wrap"), children: options.map((option, index) => (_jsx(ChoiceTab, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })) : (_jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })), tabs && _jsx(ChoiceTabNotes, { groupId: field.fieldId, options: options, chosen: options.findIndex(option => option.value === (value !== undefined ? value : defaultValue)) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
|
|
43
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode } from "react";
|
|
2
2
|
import { RowsStore, type RowsSnapshot } from "../../Helpers/RowsStore.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type InputParams, type RowsInputProps, type RowValue } from "../../Interfaces/FormInterfaces.ts";
|
|
4
4
|
import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
|
|
5
5
|
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
6
6
|
/** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
|
|
@@ -4,8 +4,10 @@ import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-ic
|
|
|
4
4
|
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, 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";
|
|
7
8
|
import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
|
|
8
9
|
import { RowsStore } from "../../Helpers/RowsStore.js";
|
|
10
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
9
11
|
import { Button } from "../Documents/Button.js";
|
|
10
12
|
import { hasContent } from "./Field.js";
|
|
11
13
|
import { isFirstInvalid } from "./InputField.js";
|
|
@@ -64,7 +66,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
64
66
|
});
|
|
65
67
|
}, [store, syncValidity]);
|
|
66
68
|
useEffect(syncValidity);
|
|
67
|
-
//
|
|
69
|
+
// Remembered row inputs (`remember`) read and keep their values in this browser: rows added from now on open on them.
|
|
70
|
+
// Given once drawn, so the server's draw and the first one in the browser are the same.
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (inputs.some(params => params.remember))
|
|
73
|
+
store.attachMemory(browserInputMemory());
|
|
74
|
+
}, [store, inputs]);
|
|
75
|
+
// Inside a form: the rows go into its data as JSON under `name`, a reset brings back the rows loaded last, and a save
|
|
76
|
+
// (a SubmitForm's) keeps the remembered values of the row added last.
|
|
68
77
|
useEffect(() => {
|
|
69
78
|
const form = proxyRef.current?.form;
|
|
70
79
|
if (!form)
|
|
@@ -79,11 +88,17 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
79
88
|
};
|
|
80
89
|
// The reset event comes before the browser restores the fields.
|
|
81
90
|
const onReset = () => setTimeout(() => store.reset());
|
|
91
|
+
const onSaved = () => {
|
|
92
|
+
if (!latest.current.disabled)
|
|
93
|
+
store.rememberAdded();
|
|
94
|
+
};
|
|
82
95
|
form.addEventListener("formdata", onFormData);
|
|
83
96
|
form.addEventListener("reset", onReset);
|
|
97
|
+
form.addEventListener(FORM_SAVED_EVENT, onSaved);
|
|
84
98
|
return () => {
|
|
85
99
|
form.removeEventListener("formdata", onFormData);
|
|
86
100
|
form.removeEventListener("reset", onReset);
|
|
101
|
+
form.removeEventListener(FORM_SAVED_EVENT, onSaved);
|
|
87
102
|
};
|
|
88
103
|
}, [store, props.form, name]);
|
|
89
104
|
function focusProblem(place) {
|
|
@@ -236,7 +251,7 @@ export function labelText(params) {
|
|
|
236
251
|
export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }) {
|
|
237
252
|
const [initial] = useState(() => store.get(rowKey, params.name));
|
|
238
253
|
const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
|
|
239
|
-
const needsName = params.type ===
|
|
254
|
+
const needsName = params.type === EInputTypes.RadioGroup || params.type === EInputTypes.CheckBoxGroup;
|
|
240
255
|
const overrides = {
|
|
241
256
|
id: `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${params.name}`,
|
|
242
257
|
// Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
|
|
@@ -257,7 +272,7 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
|
|
|
257
272
|
if (error !== undefined)
|
|
258
273
|
overrides.error = error;
|
|
259
274
|
// A required radio means one row has it on, which the rows check; each radio alone is not required.
|
|
260
|
-
if (params.type ===
|
|
275
|
+
if (params.type === EInputTypes.Radio)
|
|
261
276
|
overrides.required = false;
|
|
262
277
|
if (layout === "tabular") {
|
|
263
278
|
overrides.label = undefined;
|
|
@@ -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
|
}
|
|
@@ -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) {
|
|
@@ -359,7 +359,7 @@ export function TabularForm(props) {
|
|
|
359
359
|
const template = [
|
|
360
360
|
numbered ? numberWidth : null,
|
|
361
361
|
...columns.map((params, index) => {
|
|
362
|
-
const grow = params.grow ?? (
|
|
362
|
+
const grow = params.grow ?? (isBooleanInput(params) ? 0 : 1);
|
|
363
363
|
return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
|
|
364
364
|
}),
|
|
365
365
|
actionsWidth,
|
|
@@ -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 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,30 @@ 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;
|
|
162
207
|
/** The longest name the editor takes. Default 100. */
|
|
163
208
|
maxLabelLength?: number;
|
|
164
209
|
/** A click on a row without a checkbox, or Enter on it. Without it, a click expands or collapses a parent. */
|
|
@@ -173,6 +218,12 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
|
|
|
173
218
|
height?: number | string;
|
|
174
219
|
/** The tallest the rows' area grows before it scrolls. Default 420. */
|
|
175
220
|
maxHeight?: number | string;
|
|
221
|
+
/**
|
|
222
|
+
* Takes the height it is given, as a data table's `fill` does: the toolbar stays on top and the rows scroll inside
|
|
223
|
+
* under it, so the page around it never has to. `height` and `maxHeight` are ignored while it is on. Give it a cell
|
|
224
|
+
* that has a height: a side of an `AdminSplit` on an `AdminPage` with `fill` (nexus-admin), or a parent of your own.
|
|
225
|
+
*/
|
|
226
|
+
fill?: boolean;
|
|
176
227
|
/** Can be browsed, expanded, and focused, but not changed. A lock follows the label, and a try shows a note. */
|
|
177
228
|
readOnly?: boolean;
|
|
178
229
|
/** What a read-only tree says when someone tries to change it. Default "Read-only: this tree cannot be changed." */
|