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,168 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { formatNumber } from "./NumberHelpers.js";
|
|
3
|
+
/*
|
|
4
|
+
* When an input shows (`showWhen`), when it is read-only (`readOnlyWhen`), what it is called (`labelWhen`), how wide it
|
|
5
|
+
* is (`spanWhen`) and what it shows when it is worked out from others (`compute`): declared in its params on other
|
|
6
|
+
* inputs' values, so a page never writes a getter for `hidden`, `readOnly`, `label` or `span`. The form gives each such
|
|
7
|
+
* input a live `hidden` / `readOnly` / `label` / `span` that reads its store, and follows the inputs they name as
|
|
8
|
+
* `dependsOn` would (this file, pure, so Node tests run it).
|
|
9
|
+
*/
|
|
10
|
+
/** No value: null, undefined, an empty or blank text, an empty list. `false` and `0` are values. */
|
|
11
|
+
export function isEmptyValue(value) {
|
|
12
|
+
if (value === null || value === undefined)
|
|
13
|
+
return true;
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return value.trim() === "";
|
|
16
|
+
return Array.isArray(value) && value.length === 0;
|
|
17
|
+
}
|
|
18
|
+
/** The same value whatever its type on the way: 1 and "1" match, as a radio group's and a dropdown's do. */
|
|
19
|
+
const same = (a, b) => a === b || (a !== null && a !== undefined && b !== null && b !== undefined && String(a) === String(b));
|
|
20
|
+
/**
|
|
21
|
+
* Whether one condition holds for the other input's value. A list value (a multiple choice) holds `in` when any of its
|
|
22
|
+
* items is listed, and `notIn` when none is. Every key the condition has must hold; one with none holds always.
|
|
23
|
+
*/
|
|
24
|
+
export function conditionHolds(condition, value) {
|
|
25
|
+
const items = Array.isArray(value) ? value : [value];
|
|
26
|
+
if (condition.in && !items.some(item => condition.in.some(wanted => same(item, wanted))))
|
|
27
|
+
return false;
|
|
28
|
+
if (condition.notIn && items.some(item => condition.notIn.some(unwanted => same(item, unwanted))))
|
|
29
|
+
return false;
|
|
30
|
+
if (condition.filled !== undefined && condition.filled === isEmptyValue(value))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** The conditions of a `showWhen` or a `readOnlyWhen`, as a list. */
|
|
35
|
+
export function conditionsOf(conditions) {
|
|
36
|
+
if (!conditions)
|
|
37
|
+
return [];
|
|
38
|
+
return Array.isArray(conditions) ? conditions : [conditions];
|
|
39
|
+
}
|
|
40
|
+
/** Whether every condition holds, reading the other inputs' values with `read`. None holds always. */
|
|
41
|
+
export function conditionsHold(conditions, read) {
|
|
42
|
+
return conditionsOf(conditions).every(condition => conditionHolds(condition, read(condition.name)));
|
|
43
|
+
}
|
|
44
|
+
/** Whether an input with this `showWhen` shows, reading the other inputs' values with `read`: every condition must hold. */
|
|
45
|
+
export function inputShown(showWhen, read) {
|
|
46
|
+
return conditionsHold(showWhen, read);
|
|
47
|
+
}
|
|
48
|
+
/** Whether an input with this `readOnlyWhen` is read-only, reading the other inputs' values with `read`: every condition must hold. */
|
|
49
|
+
export function inputReadOnly(readOnlyWhen, read) {
|
|
50
|
+
return Boolean(readOnlyWhen) && conditionsHold(readOnlyWhen, read);
|
|
51
|
+
}
|
|
52
|
+
/** The text of `texts` for the other input's value (compared as text), or undefined when none is listed for it. */
|
|
53
|
+
export function textFor(texts, value) {
|
|
54
|
+
if (!texts || value === null || value === undefined || Array.isArray(value))
|
|
55
|
+
return undefined;
|
|
56
|
+
return Object.prototype.hasOwnProperty.call(texts, String(value)) ? texts[String(value)] : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The label an input with this `labelWhen` shows for the other input's value: listed, else `otherwise`, else its own. */
|
|
59
|
+
export function labelFor(labelWhen, value, own) {
|
|
60
|
+
return textFor(labelWhen.labels, value) ?? labelWhen.otherwise ?? own;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The columns an input with this `spanWhen` spans, reading the other inputs' values with `read`: `spanWhen.span` while
|
|
64
|
+
* every condition of `when` holds, else its own `span` (none: undefined, the whole row).
|
|
65
|
+
*/
|
|
66
|
+
export function spanFor(spanWhen, read, own) {
|
|
67
|
+
return spanWhen && conditionsHold(spanWhen.when, read) ? spanWhen.span : own;
|
|
68
|
+
}
|
|
69
|
+
/** The params' own value of `key` - a value or a getter, asked on the copy so `this` is the copy. */
|
|
70
|
+
function ownValue(params, copy, key) {
|
|
71
|
+
const own = Object.getOwnPropertyDescriptor(params, key);
|
|
72
|
+
return () => (own?.get ? own.get.call(copy) : own?.value);
|
|
73
|
+
}
|
|
74
|
+
/** The params' own value of `key` - a value or a getter, asked on the copy so `this` is the copy - as a boolean. */
|
|
75
|
+
function ownFlag(params, copy, key) {
|
|
76
|
+
const own = Object.getOwnPropertyDescriptor(params, key);
|
|
77
|
+
return () => Boolean(own?.get ? own.get.call(copy) : own?.value);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The inputs with a live `hidden` for each one that has `showWhen`, a live `readOnly` for each one that has
|
|
81
|
+
* `readOnlyWhen` - its own `hidden` / `readOnly` (a value or a getter), or else the conditions, read through `read` as
|
|
82
|
+
* the values are now - a live `label` (and `placeholder`) for each one that has `labelWhen`, and a live `span` for each
|
|
83
|
+
* one that has `spanWhen` (its own `span`, a value or a getter, while the conditions do not hold); and its `dependsOn`
|
|
84
|
+
* joined by the names they read (and by the inputs a `compute` is worked out from), so the form draws it again (and asks
|
|
85
|
+
* its rule again) when they change. The other params are copied with their getters as they are. The same array when no
|
|
86
|
+
* input has any.
|
|
87
|
+
*
|
|
88
|
+
* @example withInputConditions(inputs, name => store.get(name))
|
|
89
|
+
*/
|
|
90
|
+
export function withInputConditions(inputs, read) {
|
|
91
|
+
if (!inputs.some(params => params.showWhen || params.readOnlyWhen || params.labelWhen || params.spanWhen || params.compute))
|
|
92
|
+
return inputs;
|
|
93
|
+
return inputs.map(params => {
|
|
94
|
+
if (!params.showWhen && !params.readOnlyWhen && !params.labelWhen && !params.spanWhen && !params.compute)
|
|
95
|
+
return params;
|
|
96
|
+
const copy = Object.defineProperties({}, Object.getOwnPropertyDescriptors(params));
|
|
97
|
+
const names = [...conditionsOf(params.showWhen), ...conditionsOf(params.readOnlyWhen)].map(condition => condition.name);
|
|
98
|
+
if (params.labelWhen)
|
|
99
|
+
names.push(params.labelWhen.name);
|
|
100
|
+
// A width that follows values draws again when they change; its own name, when listed, follows its own typing.
|
|
101
|
+
if (params.spanWhen)
|
|
102
|
+
names.push(...conditionsOf(params.spanWhen.when).map(condition => condition.name));
|
|
103
|
+
// A worked-out input draws again, and is worked out again, when an input it is worked out from changes.
|
|
104
|
+
if (params.compute)
|
|
105
|
+
names.push(...params.compute.from);
|
|
106
|
+
const follows = [...new Set([...(params.dependsOn ?? []), ...names])];
|
|
107
|
+
if (params.showWhen) {
|
|
108
|
+
const ownHidden = ownFlag(params, copy, "hidden");
|
|
109
|
+
Object.defineProperty(copy, "hidden", { get: () => ownHidden() || !inputShown(copy.showWhen, read), enumerable: true, configurable: true });
|
|
110
|
+
}
|
|
111
|
+
if (params.readOnlyWhen) {
|
|
112
|
+
const ownReadOnly = ownFlag(params, copy, "readOnly");
|
|
113
|
+
Object.defineProperty(copy, "readOnly", { get: () => ownReadOnly() || inputReadOnly(copy.readOnlyWhen, read), enumerable: true, configurable: true });
|
|
114
|
+
}
|
|
115
|
+
if (params.labelWhen) {
|
|
116
|
+
const ownLabel = ownValue(params, copy, "label");
|
|
117
|
+
const ownPlaceholder = ownValue(params, copy, "placeholder");
|
|
118
|
+
Object.defineProperty(copy, "label", { get: () => (copy.labelWhen ? labelFor(copy.labelWhen, read(copy.labelWhen.name), ownLabel()) : ownLabel()), enumerable: true, configurable: true });
|
|
119
|
+
if (params.labelWhen.placeholders) {
|
|
120
|
+
Object.defineProperty(copy, "placeholder", { get: () => (copy.labelWhen ? (textFor(copy.labelWhen.placeholders, read(copy.labelWhen.name)) ?? ownPlaceholder()) : ownPlaceholder()), enumerable: true, configurable: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (params.spanWhen) {
|
|
124
|
+
const ownSpan = ownValue(params, copy, "span");
|
|
125
|
+
Object.defineProperty(copy, "span", { get: () => spanFor(copy.spanWhen, read, ownSpan()), enumerable: true, configurable: true });
|
|
126
|
+
}
|
|
127
|
+
Object.defineProperty(copy, "dependsOn", { value: follows, enumerable: true, configurable: true, writable: true });
|
|
128
|
+
return copy;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/* Inputs worked out from others (`compute`) */
|
|
132
|
+
/** Whether the input is worked out from others (`compute`): it holds no value of the form's and is never typed in. */
|
|
133
|
+
export function isComputedInput(params) {
|
|
134
|
+
return Boolean(params?.compute);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* What a worked-out input (`compute`) shows for these values and the form's `computeWith`. A page's rule that throws
|
|
138
|
+
* shows nothing, and says so in the console, so one bad figure never takes the form down with it.
|
|
139
|
+
*/
|
|
140
|
+
export function computedValue(params, values, withValue) {
|
|
141
|
+
if (!params.compute)
|
|
142
|
+
return undefined;
|
|
143
|
+
try {
|
|
144
|
+
return params.compute.value(values, withValue);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(`The worked-out value of "${params.name}" failed:`, error);
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A worked-out value as its box writes it: a Number input's figure with its `decimals` always written (1,073.50), grouped,
|
|
153
|
+
* in `locale` (the person's, unless the params name their own); any other input's value as text. Null, undefined, "",
|
|
154
|
+
* NaN, or a figure that is not finite, write nothing, so the box is blank rather than "NaN" or "0.00".
|
|
155
|
+
*/
|
|
156
|
+
export function computedText(params, value, locale) {
|
|
157
|
+
if (value === null || value === undefined)
|
|
158
|
+
return "";
|
|
159
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
160
|
+
return "";
|
|
161
|
+
if (params.type !== EInputTypes.Number)
|
|
162
|
+
return String(value).trim();
|
|
163
|
+
const figure = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
164
|
+
if (!Number.isFinite(figure))
|
|
165
|
+
return "";
|
|
166
|
+
const { decimals, fixedDecimals, grouping } = params;
|
|
167
|
+
return formatNumber(figure, { decimals, fixedDecimals: fixedDecimals ?? decimals !== undefined, grouping: grouping ?? true, locale: params.locale ?? locale });
|
|
168
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type InputParams, type InputRemember } from "../Interfaces/FormInterfaces.ts";
|
|
2
|
+
/** Where an input's value is kept: `nx-input:{key}:{viewer}`, or `nx-input:{key}-{per value}:{viewer}` with `per`. */
|
|
3
|
+
export declare const INPUT_MEMORY_PREFIX = "nx-input:";
|
|
4
|
+
/** Reads another value beside the input: the form's values for a form's input, the row's for an input in rows. */
|
|
5
|
+
export type InputMemoryRead = (name: string) => unknown;
|
|
6
|
+
/**
|
|
7
|
+
* The other values a remembered input's opening is judged on: the input that narrows its options (`narrowedBy`), the one
|
|
8
|
+
* its value is kept per (`per`), and the ones its `when` names. A picker's late answer opens nothing once any of them
|
|
9
|
+
* changed since it was asked: it answered for values the form no longer holds.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rememberDependencies(params: InputParams): string[];
|
|
12
|
+
/** Whether the input remembers among these values: its `when` holds (none: always). */
|
|
13
|
+
export declare function remembersHere(remember: InputRemember | null | undefined, read?: InputMemoryRead): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The storage key of a remembered input, or null when nothing is kept here: no key, no viewer to keep it for, its `when`
|
|
16
|
+
* does not hold, or its `per` input holds no single value. With `per`, the other value ends the key: one value kept per
|
|
17
|
+
* group, `nx-input:subscription-package-{group}:{viewer}`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inputMemoryKey(remember: InputRemember | null | undefined, read?: InputMemoryRead): string | null;
|
|
20
|
+
/** What `getItem`/`setItem`/`removeItem` a memory needs: `localStorage`, or a map in a test. */
|
|
21
|
+
export type InputMemoryStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
22
|
+
/** This browser's local storage, or nothing where it cannot be reached (on a server, storage switched off). */
|
|
23
|
+
export declare function browserInputMemory(): InputMemoryStorage | null;
|
|
24
|
+
/**
|
|
25
|
+
* The event a `SubmitForm` sends its `<form>` once a save went through, before the form resets: the rows inside it keep
|
|
26
|
+
* their remembered values then (`RowsStore.rememberAdded`), as the form's own inputs do.
|
|
27
|
+
*/
|
|
28
|
+
export declare const FORM_SAVED_EVENT = "nx-form-saved";
|
|
29
|
+
/** The value kept for this input, or undefined: none kept, no viewer, or storage that cannot be read. */
|
|
30
|
+
export declare function readInputMemory(storage: InputMemoryStorage | null, remember: InputRemember | null | undefined, read?: InputMemoryRead): unknown;
|
|
31
|
+
/** Keeps a saved value for the input; an empty one forgets it. Storage that cannot be written keeps nothing. */
|
|
32
|
+
export declare function writeInputMemory(storage: InputMemoryStorage | null, params: InputParams, value: unknown, read?: InputMemoryRead): void;
|
|
33
|
+
/** Forgets the input's kept value: one its list no longer offers. */
|
|
34
|
+
export declare function forgetInputMemory(storage: InputMemoryStorage | null, remember: InputRemember | null | undefined, read?: InputMemoryRead): void;
|
|
35
|
+
/** Whether a value is the input's empty one: nothing, "", an empty list, or a switch that is off. */
|
|
36
|
+
export declare function isEmptyInputValue(params: InputParams, value: unknown): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The part of a value the input offers now, or undefined when it offers none of it. An input with a list of its own
|
|
39
|
+
* (radio and checkbox groups, a dropdown's `options`) must list it, neither disabled nor inactive; a list of values
|
|
40
|
+
* keeps the ones it lists. An input whose options come from a server, or that has no options, takes any value here: a
|
|
41
|
+
* picker is asked by `keptStandingNow` / `findKeptStanding`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function offeredInputValue(params: InputParams, value: unknown): unknown;
|
|
44
|
+
/** A dropdown whose options come from `loadOptions` and that has no list of its own: it is asked what it offers. */
|
|
45
|
+
export declare function isPickerInput(params: InputParams): boolean;
|
|
46
|
+
/** How a kept (or preferred) value stands with its input now. */
|
|
47
|
+
export type KeptStanding =
|
|
48
|
+
/** Offered: open on it (for a list of values, the part offered). */
|
|
49
|
+
{
|
|
50
|
+
status: "offered";
|
|
51
|
+
value: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** Not offered - gone, inactive, disabled, or outside the narrowing: open nothing, and forget a kept value. */
|
|
54
|
+
| {
|
|
55
|
+
status: "gone";
|
|
56
|
+
}
|
|
57
|
+
/** Could not be told now (the picker could not be read, nothing to narrow by yet): open nothing, keep it for next time. */
|
|
58
|
+
| {
|
|
59
|
+
status: "unknown";
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* How a value stands with the input at once, with no call: an input's own list (narrowed as its `narrowedBy` says among
|
|
63
|
+
* these values) offers it or not; a picker by what it holds already (`peekOption`, fresh only); any other input takes
|
|
64
|
+
* it. Null when a picker must be asked (`findKeptStanding`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function keptStandingNow(params: InputParams, value: unknown, read?: InputMemoryRead): KeptStanding | null;
|
|
67
|
+
/**
|
|
68
|
+
* How a value stands with the input, asking a picker when it must: by the value's key (`resolveOptions`, as a saved
|
|
69
|
+
* value is named) - a record found is judged on its own state and narrowing field - and, for a key it did not answer or
|
|
70
|
+
* a record without its narrowing's field, through its own pages narrowed as the form narrows them (a picker that names
|
|
71
|
+
* only what it already holds, such as a `crudPicker` with another `valueKey` and no `find`, answers nothing for a list
|
|
72
|
+
* not loaded yet: its pages tell). Never rejects: a picker that cannot be read answers `unknown`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findKeptStanding(params: InputParams, value: unknown, read: InputMemoryRead, signal: AbortSignal): Promise<KeptStanding>;
|
|
75
|
+
/** What a remembered input opens on. */
|
|
76
|
+
export interface KeptOpening {
|
|
77
|
+
/** The value it opens on; undefined: none (it keeps its own default). */
|
|
78
|
+
value: unknown;
|
|
79
|
+
/** The kept choice is no longer offered: forget it. */
|
|
80
|
+
forget: boolean;
|
|
81
|
+
/** A picker must be asked first (`findRememberedOpening`); nothing opens until it answers. */
|
|
82
|
+
ask: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What a remembered input opens on among these values, at once where it can be told: the company's `preferred` value
|
|
86
|
+
* while the input offers it, else the person's last saved choice while it offers that, else none. `ask` when a picker
|
|
87
|
+
* must be asked first (`findRememberedOpening`). Nothing while its `when` does not hold.
|
|
88
|
+
*/
|
|
89
|
+
export declare function rememberedOpeningNow(params: InputParams, stored: unknown, read?: InputMemoryRead): KeptOpening;
|
|
90
|
+
/**
|
|
91
|
+
* The same, asking a picker by the values' keys where it must (`findKeptStanding`). Never rejects: a picker that could
|
|
92
|
+
* not be read opens nothing and forgets nothing.
|
|
93
|
+
*/
|
|
94
|
+
export declare function findRememberedOpening(params: InputParams, stored: unknown, read: InputMemoryRead, signal: AbortSignal): Promise<KeptOpening>;
|
|
95
|
+
/**
|
|
96
|
+
* What a remembered input opens on when the form's values leave it empty, from its own list alone: the company's
|
|
97
|
+
* `preferred` value while the input offers it, else the person's last saved choice while it offers that, else undefined
|
|
98
|
+
* (the input keeps its own default). A value no longer offered is dropped without a word, never shown as a broken one.
|
|
99
|
+
* A picker's values are taken as they are here; a form asks the picker (`rememberedOpeningNow`).
|
|
100
|
+
*/
|
|
101
|
+
export declare function rememberedOpening(params: InputParams, stored: unknown): unknown;
|
|
102
|
+
/**
|
|
103
|
+
* The row of a list a save keeps a row input's value from: the one added last that is still there, else none (no row
|
|
104
|
+
* was added, so no new choice was made). Keys in the order they were added.
|
|
105
|
+
*/
|
|
106
|
+
export declare function lastAddedRowKey(added: readonly string[], present: (key: string) => boolean): string | null;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { isDisabledItem, isInactiveItem, optionKey, readField } from "./DropdownHelpers.js";
|
|
3
|
+
import { conditionsHold, conditionsOf } from "./InputConditions.js";
|
|
4
|
+
import { narrowInputParams, narrowingValue } from "./InputNarrowing.js";
|
|
5
|
+
import { emptyInputValue, inputValuesEqual } from "./InputParamsHelpers.js";
|
|
6
|
+
/*
|
|
7
|
+
* An input's last saved value, kept in this browser per viewer (`InputLayoutParams.remember`): what a form opens an
|
|
8
|
+
* input on when its own values leave it empty - the company's preferred value first, then the person's last choice, each
|
|
9
|
+
* only while the input still offers it. A picker (a dropdown whose options come from `loadOptions`) is asked first, by
|
|
10
|
+
* the value's key, whether it still offers it; a kept choice it no longer offers is forgotten. Pure: the storage is
|
|
11
|
+
* passed in, so a test runs on a map.
|
|
12
|
+
*/
|
|
13
|
+
/** Where an input's value is kept: `nx-input:{key}:{viewer}`, or `nx-input:{key}-{per value}:{viewer}` with `per`. */
|
|
14
|
+
export const INPUT_MEMORY_PREFIX = "nx-input:";
|
|
15
|
+
const readNothing = () => undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The other values a remembered input's opening is judged on: the input that narrows its options (`narrowedBy`), the one
|
|
18
|
+
* its value is kept per (`per`), and the ones its `when` names. A picker's late answer opens nothing once any of them
|
|
19
|
+
* changed since it was asked: it answered for values the form no longer holds.
|
|
20
|
+
*/
|
|
21
|
+
export function rememberDependencies(params) {
|
|
22
|
+
const names = new Set();
|
|
23
|
+
if (params.narrowedBy?.name !== undefined)
|
|
24
|
+
names.add(params.narrowedBy.name);
|
|
25
|
+
if (params.remember?.per)
|
|
26
|
+
names.add(params.remember.per);
|
|
27
|
+
for (const condition of conditionsOf(params.remember?.when))
|
|
28
|
+
names.add(condition.name);
|
|
29
|
+
names.delete(params.name);
|
|
30
|
+
return [...names];
|
|
31
|
+
}
|
|
32
|
+
/** Whether the input remembers among these values: its `when` holds (none: always). */
|
|
33
|
+
export function remembersHere(remember, read = readNothing) {
|
|
34
|
+
if (!remember)
|
|
35
|
+
return false;
|
|
36
|
+
return !remember.when || conditionsHold(remember.when, read);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The storage key of a remembered input, or null when nothing is kept here: no key, no viewer to keep it for, its `when`
|
|
40
|
+
* does not hold, or its `per` input holds no single value. With `per`, the other value ends the key: one value kept per
|
|
41
|
+
* group, `nx-input:subscription-package-{group}:{viewer}`.
|
|
42
|
+
*/
|
|
43
|
+
export function inputMemoryKey(remember, read = readNothing) {
|
|
44
|
+
if (!remember?.key || !remember.viewer || !remembersHere(remember, read))
|
|
45
|
+
return null;
|
|
46
|
+
if (!remember.per)
|
|
47
|
+
return `${INPUT_MEMORY_PREFIX}${remember.key}:${remember.viewer}`;
|
|
48
|
+
const per = narrowingValue(read(remember.per));
|
|
49
|
+
return per === null ? null : `${INPUT_MEMORY_PREFIX}${remember.key}-${String(per)}:${remember.viewer}`;
|
|
50
|
+
}
|
|
51
|
+
/** This browser's local storage, or nothing where it cannot be reached (on a server, storage switched off). */
|
|
52
|
+
export function browserInputMemory() {
|
|
53
|
+
try {
|
|
54
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The event a `SubmitForm` sends its `<form>` once a save went through, before the form resets: the rows inside it keep
|
|
62
|
+
* their remembered values then (`RowsStore.rememberAdded`), as the form's own inputs do.
|
|
63
|
+
*/
|
|
64
|
+
export const FORM_SAVED_EVENT = "nx-form-saved";
|
|
65
|
+
/** The value kept for this input, or undefined: none kept, no viewer, or storage that cannot be read. */
|
|
66
|
+
export function readInputMemory(storage, remember, read = readNothing) {
|
|
67
|
+
const key = inputMemoryKey(remember, read);
|
|
68
|
+
if (!storage || !key)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const text = storage.getItem(key);
|
|
72
|
+
if (!text)
|
|
73
|
+
return undefined;
|
|
74
|
+
const kept = JSON.parse(text);
|
|
75
|
+
return kept && typeof kept === "object" ? kept.value : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Keeps a saved value for the input; an empty one forgets it. Storage that cannot be written keeps nothing. */
|
|
82
|
+
export function writeInputMemory(storage, params, value, read = readNothing) {
|
|
83
|
+
const key = inputMemoryKey(params.remember, read);
|
|
84
|
+
if (!storage || !key)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
if (isEmptyInputValue(params, value))
|
|
88
|
+
storage.removeItem(key);
|
|
89
|
+
else
|
|
90
|
+
storage.setItem(key, JSON.stringify({ value }));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Storage full or blocked: the form works without it.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Forgets the input's kept value: one its list no longer offers. */
|
|
97
|
+
export function forgetInputMemory(storage, remember, read = readNothing) {
|
|
98
|
+
const key = inputMemoryKey(remember, read);
|
|
99
|
+
if (!storage || !key)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
storage.removeItem(key);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Blocked: it is asked about again next time.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Whether a value is the input's empty one: nothing, "", an empty list, or a switch that is off. */
|
|
109
|
+
export function isEmptyInputValue(params, value) {
|
|
110
|
+
if (value === undefined || value === null || value === "")
|
|
111
|
+
return true;
|
|
112
|
+
if (Array.isArray(value) && value.length === 0)
|
|
113
|
+
return true;
|
|
114
|
+
return inputValuesEqual(value, emptyInputValue(params));
|
|
115
|
+
}
|
|
116
|
+
/** An input's own list of options, when it holds one: a choice group's, a dropdown's `options` (not a server's pages). */
|
|
117
|
+
function listedOptions(params) {
|
|
118
|
+
const options = params.options;
|
|
119
|
+
return Array.isArray(options) ? options : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The part of a value the input offers now, or undefined when it offers none of it. An input with a list of its own
|
|
123
|
+
* (radio and checkbox groups, a dropdown's `options`) must list it, neither disabled nor inactive; a list of values
|
|
124
|
+
* keeps the ones it lists. An input whose options come from a server, or that has no options, takes any value here: a
|
|
125
|
+
* picker is asked by `keptStandingNow` / `findKeptStanding`.
|
|
126
|
+
*/
|
|
127
|
+
export function offeredInputValue(params, value) {
|
|
128
|
+
if (isEmptyInputValue(params, value))
|
|
129
|
+
return undefined;
|
|
130
|
+
const options = listedOptions(params);
|
|
131
|
+
if (!options)
|
|
132
|
+
return value;
|
|
133
|
+
const valueKey = params.type === EInputTypes.Dropdown ? (params.valueKey ?? "value") : "value";
|
|
134
|
+
const inactive = params.isOptionInactive;
|
|
135
|
+
const offered = new Set(options
|
|
136
|
+
.filter(item => item && typeof item === "object" && !isDisabledItem(item) && !(inactive ? inactive(item) : isInactiveItem(item)))
|
|
137
|
+
.map(item => optionKey(readField(item, valueKey))));
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const kept = value.filter(one => offered.has(optionKey(one)));
|
|
140
|
+
return kept.length ? kept : undefined;
|
|
141
|
+
}
|
|
142
|
+
return offered.has(optionKey(value)) ? value : undefined;
|
|
143
|
+
}
|
|
144
|
+
/** A dropdown whose options come from `loadOptions` and that has no list of its own: it is asked what it offers. */
|
|
145
|
+
export function isPickerInput(params) {
|
|
146
|
+
return params.type === EInputTypes.Dropdown && !listedOptions(params) && typeof params.loadOptions === "function";
|
|
147
|
+
}
|
|
148
|
+
/** The narrowing an input's options follow among these values (`narrowedBy`), or null when they follow none. */
|
|
149
|
+
function narrowingAmong(params, read) {
|
|
150
|
+
const narrowing = params.narrowedBy;
|
|
151
|
+
if (!narrowing)
|
|
152
|
+
return null;
|
|
153
|
+
const fixed = narrowing.name === undefined;
|
|
154
|
+
const value = narrowingValue(fixed ? narrowing.value : read(narrowing.name));
|
|
155
|
+
return { key: narrowing.key ?? narrowing.name ?? "", value, orUnset: narrowing.orUnset === true };
|
|
156
|
+
}
|
|
157
|
+
const GONE = { status: "gone" };
|
|
158
|
+
const UNKNOWN = { status: "unknown" };
|
|
159
|
+
/** A value as its keys: a list's items, or the one value. */
|
|
160
|
+
function keysOf(value) {
|
|
161
|
+
return (Array.isArray(value) ? value : [value]).map(optionKey).filter(key => key !== "");
|
|
162
|
+
}
|
|
163
|
+
/** The part of `value` whose keys are offered, in its own shape: the value, a shorter list, or undefined for none. */
|
|
164
|
+
function offeredPart(value, offered) {
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
const kept = value.filter(one => offered.has(optionKey(one)));
|
|
167
|
+
return kept.length ? kept : undefined;
|
|
168
|
+
}
|
|
169
|
+
return offered.has(optionKey(value)) ? value : undefined;
|
|
170
|
+
}
|
|
171
|
+
/** A record the picker would let a person pick, its narrowing aside: not disabled, not inactive. */
|
|
172
|
+
function pickable(params, record) {
|
|
173
|
+
const own = params;
|
|
174
|
+
return !isDisabledItem(record) && !(own.isOptionInactive ? own.isOptionInactive(record) : isInactiveItem(record));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether a record the picker answered is one it offers among these values: not disabled, not inactive (its
|
|
178
|
+
* `isOptionInactive`, else `inactive`/`isActive: false`), and inside its narrowing - the record's field holds the other
|
|
179
|
+
* input's value (or is empty, with `orUnset`). Undefined when the record does not carry the narrowing's field: only the
|
|
180
|
+
* picker's narrowed pages can tell then.
|
|
181
|
+
*/
|
|
182
|
+
function offersRecord(params, record, read) {
|
|
183
|
+
if (!pickable(params, record))
|
|
184
|
+
return false;
|
|
185
|
+
const narrowing = narrowingAmong(params, read);
|
|
186
|
+
if (!narrowing)
|
|
187
|
+
return true;
|
|
188
|
+
if (narrowing.value === null && !narrowing.orUnset)
|
|
189
|
+
return false;
|
|
190
|
+
if (!(narrowing.key in record))
|
|
191
|
+
return undefined;
|
|
192
|
+
const field = record[narrowing.key];
|
|
193
|
+
return (narrowing.value !== null && String(field) === String(narrowing.value)) || (narrowing.orUnset && (field === null || field === undefined || field === ""));
|
|
194
|
+
}
|
|
195
|
+
/** The records among `items` by key, each judged: true offered, false not, undefined not to be told from the record. */
|
|
196
|
+
function judge(params, items, keys, read) {
|
|
197
|
+
const valueKey = params.valueKey ?? "value";
|
|
198
|
+
const found = new Map();
|
|
199
|
+
for (const key of keys) {
|
|
200
|
+
const record = items.find(item => optionKey(item[valueKey]) === key);
|
|
201
|
+
if (record)
|
|
202
|
+
found.set(key, offersRecord(params, record, read));
|
|
203
|
+
}
|
|
204
|
+
return found;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* How a value stands with the input at once, with no call: an input's own list (narrowed as its `narrowedBy` says among
|
|
208
|
+
* these values) offers it or not; a picker by what it holds already (`peekOption`, fresh only); any other input takes
|
|
209
|
+
* it. Null when a picker must be asked (`findKeptStanding`).
|
|
210
|
+
*/
|
|
211
|
+
export function keptStandingNow(params, value, read = readNothing) {
|
|
212
|
+
if (isEmptyInputValue(params, value))
|
|
213
|
+
return GONE;
|
|
214
|
+
// Nothing to narrow by yet (the other input is empty): judged once it has a value, so neither opened nor forgotten.
|
|
215
|
+
const narrowing = narrowingAmong(params, read);
|
|
216
|
+
if (narrowing && narrowing.value === null && !narrowing.orUnset)
|
|
217
|
+
return UNKNOWN;
|
|
218
|
+
if (!isPickerInput(params)) {
|
|
219
|
+
const offered = offeredInputValue(narrowInputParams(params, params.narrowedBy?.name === undefined ? undefined : read(params.narrowedBy.name)), value);
|
|
220
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
221
|
+
}
|
|
222
|
+
const peek = params.peekOption;
|
|
223
|
+
if (!peek)
|
|
224
|
+
return null;
|
|
225
|
+
const keys = keysOf(value);
|
|
226
|
+
const records = keys.map(key => peek(key)).filter((record) => Boolean(record));
|
|
227
|
+
const judged = judge(params, records, keys, read);
|
|
228
|
+
if (keys.some(key => judged.get(key) === undefined))
|
|
229
|
+
return null;
|
|
230
|
+
const offered = offeredPart(value, new Set(keys.filter(key => judged.get(key) === true)));
|
|
231
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
232
|
+
}
|
|
233
|
+
/** The page size a kept value is looked for in, when the picker cannot name a record by its key. */
|
|
234
|
+
const FIND_PAGE_SIZE = 50;
|
|
235
|
+
/**
|
|
236
|
+
* Pages looked through for a kept value. A key not seen in them is no longer offered only when the list ended there; a
|
|
237
|
+
* longer list leaves it untold (`unknown`: neither opened nor forgotten).
|
|
238
|
+
*/
|
|
239
|
+
const FIND_PAGES = 20;
|
|
240
|
+
/**
|
|
241
|
+
* The keys the picker's own pages (narrowed as the form narrows them) offer, read until each key showed or they end.
|
|
242
|
+
* A page ends the list as the box itself reads one: `hasMore: false`, an empty page, or (no `hasMore`) fewer items than
|
|
243
|
+
* the page size asked - the box would offer nothing past it either.
|
|
244
|
+
*/
|
|
245
|
+
async function offeredInPages(params, keys, read, signal) {
|
|
246
|
+
const narrowed = narrowInputParams(params, params.narrowedBy?.name === undefined ? undefined : read(params.narrowedBy.name));
|
|
247
|
+
const load = narrowed.loadOptions;
|
|
248
|
+
const offered = new Set();
|
|
249
|
+
if (!load)
|
|
250
|
+
return { offered, ended: true };
|
|
251
|
+
const valueKey = narrowed.valueKey ?? "value";
|
|
252
|
+
const wanted = new Set(keys);
|
|
253
|
+
for (let page = 1; page <= FIND_PAGES && wanted.size; page++) {
|
|
254
|
+
if (signal.aborted)
|
|
255
|
+
break;
|
|
256
|
+
const skip = (page - 1) * FIND_PAGE_SIZE;
|
|
257
|
+
const answer = await load({ search: "", page, skip, pageSize: FIND_PAGE_SIZE, signal });
|
|
258
|
+
for (const item of answer.items) {
|
|
259
|
+
const key = optionKey(item[valueKey]);
|
|
260
|
+
if (!wanted.has(key))
|
|
261
|
+
continue;
|
|
262
|
+
wanted.delete(key);
|
|
263
|
+
// The narrowed pages hold only the narrowing's records: one read there is judged on its own state alone.
|
|
264
|
+
if (pickable(params, item))
|
|
265
|
+
offered.add(key);
|
|
266
|
+
}
|
|
267
|
+
const more = answer.hasMore ?? answer.items.length >= FIND_PAGE_SIZE;
|
|
268
|
+
if (!more || answer.items.length === 0)
|
|
269
|
+
return { offered, ended: true };
|
|
270
|
+
}
|
|
271
|
+
// Every key showed (the rest of the list is not needed), or the pages ran out (or the ask stopped) before the end.
|
|
272
|
+
return { offered, ended: wanted.size === 0 };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* How a value stands with the input, asking a picker when it must: by the value's key (`resolveOptions`, as a saved
|
|
276
|
+
* value is named) - a record found is judged on its own state and narrowing field - and, for a key it did not answer or
|
|
277
|
+
* a record without its narrowing's field, through its own pages narrowed as the form narrows them (a picker that names
|
|
278
|
+
* only what it already holds, such as a `crudPicker` with another `valueKey` and no `find`, answers nothing for a list
|
|
279
|
+
* not loaded yet: its pages tell). Never rejects: a picker that cannot be read answers `unknown`.
|
|
280
|
+
*/
|
|
281
|
+
export async function findKeptStanding(params, value, read, signal) {
|
|
282
|
+
const now = keptStandingNow(params, value, read);
|
|
283
|
+
if (now)
|
|
284
|
+
return now;
|
|
285
|
+
const own = params;
|
|
286
|
+
const keys = keysOf(value);
|
|
287
|
+
try {
|
|
288
|
+
let judged = new Map();
|
|
289
|
+
if (own.resolveOptions) {
|
|
290
|
+
const records = await own.resolveOptions(keys);
|
|
291
|
+
if (signal.aborted)
|
|
292
|
+
return UNKNOWN;
|
|
293
|
+
judged = judge(params, records, keys, read);
|
|
294
|
+
}
|
|
295
|
+
const untold = keys.filter(key => judged.get(key) === undefined);
|
|
296
|
+
if (untold.length) {
|
|
297
|
+
if (signal.aborted)
|
|
298
|
+
return UNKNOWN;
|
|
299
|
+
const pages = await offeredInPages(params, untold, read, signal);
|
|
300
|
+
if (signal.aborted)
|
|
301
|
+
return UNKNOWN;
|
|
302
|
+
// A key not seen before the pages ran out may stand further on: not known, so neither opened nor forgotten.
|
|
303
|
+
if (!pages.ended && untold.some(key => !pages.offered.has(key)))
|
|
304
|
+
return UNKNOWN;
|
|
305
|
+
for (const key of untold)
|
|
306
|
+
judged.set(key, pages.offered.has(key));
|
|
307
|
+
}
|
|
308
|
+
const offered = offeredPart(value, new Set(keys.filter(key => judged.get(key) === true)));
|
|
309
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return UNKNOWN;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const NO_OPENING = { value: undefined, forget: false, ask: false };
|
|
316
|
+
/**
|
|
317
|
+
* What a remembered input opens on among these values, at once where it can be told: the company's `preferred` value
|
|
318
|
+
* while the input offers it, else the person's last saved choice while it offers that, else none. `ask` when a picker
|
|
319
|
+
* must be asked first (`findRememberedOpening`). Nothing while its `when` does not hold.
|
|
320
|
+
*/
|
|
321
|
+
export function rememberedOpeningNow(params, stored, read = readNothing) {
|
|
322
|
+
const remember = params.remember;
|
|
323
|
+
if (!remember || !remembersHere(remember, read))
|
|
324
|
+
return NO_OPENING;
|
|
325
|
+
if (!isEmptyInputValue(params, remember.preferred)) {
|
|
326
|
+
const standing = keptStandingNow(params, remember.preferred, read);
|
|
327
|
+
if (!standing)
|
|
328
|
+
return { ...NO_OPENING, ask: true };
|
|
329
|
+
if (standing.status === "offered")
|
|
330
|
+
return { value: standing.value, forget: false, ask: false };
|
|
331
|
+
}
|
|
332
|
+
if (isEmptyInputValue(params, stored))
|
|
333
|
+
return NO_OPENING;
|
|
334
|
+
const standing = keptStandingNow(params, stored, read);
|
|
335
|
+
if (!standing)
|
|
336
|
+
return { ...NO_OPENING, ask: true };
|
|
337
|
+
if (standing.status === "offered")
|
|
338
|
+
return { value: standing.value, forget: false, ask: false };
|
|
339
|
+
return { value: undefined, forget: standing.status === "gone", ask: false };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The same, asking a picker by the values' keys where it must (`findKeptStanding`). Never rejects: a picker that could
|
|
343
|
+
* not be read opens nothing and forgets nothing.
|
|
344
|
+
*/
|
|
345
|
+
export async function findRememberedOpening(params, stored, read, signal) {
|
|
346
|
+
const remember = params.remember;
|
|
347
|
+
if (!remember || !remembersHere(remember, read))
|
|
348
|
+
return NO_OPENING;
|
|
349
|
+
if (!isEmptyInputValue(params, remember.preferred)) {
|
|
350
|
+
const standing = await findKeptStanding(params, remember.preferred, read, signal);
|
|
351
|
+
if (standing.status === "offered")
|
|
352
|
+
return { value: standing.value, forget: false, ask: false };
|
|
353
|
+
}
|
|
354
|
+
if (isEmptyInputValue(params, stored) || signal.aborted)
|
|
355
|
+
return NO_OPENING;
|
|
356
|
+
const standing = await findKeptStanding(params, stored, read, signal);
|
|
357
|
+
if (standing.status === "offered")
|
|
358
|
+
return { value: standing.value, forget: false, ask: false };
|
|
359
|
+
return { value: undefined, forget: standing.status === "gone", ask: false };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* What a remembered input opens on when the form's values leave it empty, from its own list alone: the company's
|
|
363
|
+
* `preferred` value while the input offers it, else the person's last saved choice while it offers that, else undefined
|
|
364
|
+
* (the input keeps its own default). A value no longer offered is dropped without a word, never shown as a broken one.
|
|
365
|
+
* A picker's values are taken as they are here; a form asks the picker (`rememberedOpeningNow`).
|
|
366
|
+
*/
|
|
367
|
+
export function rememberedOpening(params, stored) {
|
|
368
|
+
const remember = params.remember;
|
|
369
|
+
if (!remember)
|
|
370
|
+
return undefined;
|
|
371
|
+
const preferred = offeredInputValue(params, remember.preferred);
|
|
372
|
+
if (preferred !== undefined)
|
|
373
|
+
return preferred;
|
|
374
|
+
return offeredInputValue(params, stored);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The row of a list a save keeps a row input's value from: the one added last that is still there, else none (no row
|
|
378
|
+
* was added, so no new choice was made). Keys in the order they were added.
|
|
379
|
+
*/
|
|
380
|
+
export function lastAddedRowKey(added, present) {
|
|
381
|
+
for (let index = added.length - 1; index >= 0; index--)
|
|
382
|
+
if (present(added[index]))
|
|
383
|
+
return added[index];
|
|
384
|
+
return null;
|
|
385
|
+
}
|