nexus-shared 1.1.20 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { deleteFormDraft, readFormDraft, writeFormDraft } from "./FormDrafts.js";
|
|
2
|
+
import { initialInputValue, inputValuesEqual, isRowsInput, normalizeInputValue, validateInput } from "./InputParamsHelpers.js";
|
|
3
|
+
const INITIAL_STATUS = {
|
|
4
|
+
changed: false,
|
|
5
|
+
submitting: false,
|
|
6
|
+
loading: false,
|
|
7
|
+
showAll: false,
|
|
8
|
+
problems: [],
|
|
9
|
+
message: null,
|
|
10
|
+
savedAt: null,
|
|
11
|
+
draftSavedAt: null,
|
|
12
|
+
restored: null,
|
|
13
|
+
formId: null,
|
|
14
|
+
buttons: 0,
|
|
15
|
+
};
|
|
16
|
+
/** How long values rest before a draft is written, in milliseconds. */
|
|
17
|
+
const DRAFT_DELAY = 600;
|
|
18
|
+
/** Equal plain data, however deep: what a draft or a reload compares, where arrays and objects are new copies. */
|
|
19
|
+
function sameData(a, b) {
|
|
20
|
+
if (Object.is(a, b))
|
|
21
|
+
return true;
|
|
22
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
23
|
+
return false;
|
|
24
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
25
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
26
|
+
return false;
|
|
27
|
+
return a.every((item, index) => sameData(item, b[index]));
|
|
28
|
+
}
|
|
29
|
+
const keysA = Object.keys(a);
|
|
30
|
+
const keysB = Object.keys(b);
|
|
31
|
+
if (keysA.length !== keysB.length)
|
|
32
|
+
return false;
|
|
33
|
+
return keysA.every(key => sameData(a[key], b[key]));
|
|
34
|
+
}
|
|
35
|
+
function sameList(a, b) {
|
|
36
|
+
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
37
|
+
}
|
|
38
|
+
export class FormStore {
|
|
39
|
+
options = { inputs: [] };
|
|
40
|
+
inputByName = new Map();
|
|
41
|
+
values = {};
|
|
42
|
+
initial = {};
|
|
43
|
+
isStarted = false;
|
|
44
|
+
// Messages: an input's own rules, the form's rule, a server's, and rule messages the form shows for its inputs.
|
|
45
|
+
ruleErrors = new Map();
|
|
46
|
+
/** Rows inputs are checked when asked: 10,000 rows on every keystroke would be slow, and the rows check themselves. */
|
|
47
|
+
staleRules = new Set();
|
|
48
|
+
crossErrors = new Map();
|
|
49
|
+
crossMessage = null;
|
|
50
|
+
serverErrors = new Map();
|
|
51
|
+
serverMessage = null;
|
|
52
|
+
forced = new Set();
|
|
53
|
+
touched = new Set();
|
|
54
|
+
showAll = false;
|
|
55
|
+
/** Inputs the page found problems in on the last submit. */
|
|
56
|
+
attention = [];
|
|
57
|
+
changedNames = new Set();
|
|
58
|
+
// Rendering: a revision per value set from code (its input mounts again with it), a generation for every input.
|
|
59
|
+
revisions = new Map();
|
|
60
|
+
generation = 0;
|
|
61
|
+
versions = new Map();
|
|
62
|
+
fieldListeners = new Map();
|
|
63
|
+
statusListeners = new Set();
|
|
64
|
+
// Kept without the values' type, so a store of any values passes where a store of plain values is asked for.
|
|
65
|
+
valueListeners = new Set();
|
|
66
|
+
status = INITIAL_STATUS;
|
|
67
|
+
draftTimer;
|
|
68
|
+
/** The draft key already looked up. Drafts are written only after, so an old draft is never overwritten unread. */
|
|
69
|
+
draftChecked = null;
|
|
70
|
+
draftSeq = 0;
|
|
71
|
+
/** The user cancelled: no draft is written until they change a value again. */
|
|
72
|
+
draftDropped = false;
|
|
73
|
+
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
74
|
+
quiet = false;
|
|
75
|
+
unannounced = false;
|
|
76
|
+
/** The form on the page. Set by `Form`. */
|
|
77
|
+
view = null;
|
|
78
|
+
/** Submits the `<form>`, as its Save button does. Set by `SubmitForm`. */
|
|
79
|
+
submitter = null;
|
|
80
|
+
/* Reading */
|
|
81
|
+
/** Whether the form has loaded its first values. */
|
|
82
|
+
get started() {
|
|
83
|
+
return this.isStarted;
|
|
84
|
+
}
|
|
85
|
+
get inputs() {
|
|
86
|
+
return this.options.inputs;
|
|
87
|
+
}
|
|
88
|
+
/** Every value, plus the fields the record came with. The same object until something changes; do not change it. */
|
|
89
|
+
getValue() {
|
|
90
|
+
return this.values;
|
|
91
|
+
}
|
|
92
|
+
get(name) {
|
|
93
|
+
return this.values[name];
|
|
94
|
+
}
|
|
95
|
+
/** The values the form opened with, or saved last. */
|
|
96
|
+
getInitial() {
|
|
97
|
+
return this.initial;
|
|
98
|
+
}
|
|
99
|
+
/** Only the values that differ from the ones the form opened with or saved last. */
|
|
100
|
+
getChanges() {
|
|
101
|
+
const changes = {};
|
|
102
|
+
for (const name of this.changedInputs())
|
|
103
|
+
changes[name] = this.values[name];
|
|
104
|
+
return changes;
|
|
105
|
+
}
|
|
106
|
+
/** The inputs whose values changed, in form order. */
|
|
107
|
+
changedInputs() {
|
|
108
|
+
return this.options.inputs.filter(params => this.changedNames.has(params.name)).map(params => params.name);
|
|
109
|
+
}
|
|
110
|
+
isChanged() {
|
|
111
|
+
return this.changedNames.size > 0;
|
|
112
|
+
}
|
|
113
|
+
getStatus = () => this.status;
|
|
114
|
+
/** The message an input shows for the form: a server's, the form's rule once the input was left or a submit ran, or its own rule after a submit its input missed. */
|
|
115
|
+
shownError(name) {
|
|
116
|
+
return this.shownWith(name, this.crossErrors);
|
|
117
|
+
}
|
|
118
|
+
/** The shown message, with the form's rule messages given, such as the ones before a change. */
|
|
119
|
+
shownWith(name, crossErrors) {
|
|
120
|
+
const server = this.serverErrors.get(name);
|
|
121
|
+
if (server)
|
|
122
|
+
return server;
|
|
123
|
+
const cross = crossErrors.get(name);
|
|
124
|
+
if (cross && (this.showAll || this.touched.has(name)))
|
|
125
|
+
return cross;
|
|
126
|
+
if (this.forced.has(name))
|
|
127
|
+
return this.ruleError(name) ?? undefined;
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/** The input's own rule message for its value, whether it shows or not. */
|
|
131
|
+
ruleError(name) {
|
|
132
|
+
if (this.staleRules.has(name)) {
|
|
133
|
+
this.staleRules.delete(name);
|
|
134
|
+
const params = this.inputByName.get(name);
|
|
135
|
+
const message = params && !params.hidden && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
|
|
136
|
+
if (message)
|
|
137
|
+
this.ruleErrors.set(name, message);
|
|
138
|
+
else
|
|
139
|
+
this.ruleErrors.delete(name);
|
|
140
|
+
}
|
|
141
|
+
return this.ruleErrors.get(name) ?? null;
|
|
142
|
+
}
|
|
143
|
+
/** The form's own rule message for an input, whether it shows or not. */
|
|
144
|
+
formRuleError(name) {
|
|
145
|
+
return this.crossErrors.get(name) ?? null;
|
|
146
|
+
}
|
|
147
|
+
/** The form's own rule message about the whole form. */
|
|
148
|
+
get formRuleMessage() {
|
|
149
|
+
return this.crossMessage;
|
|
150
|
+
}
|
|
151
|
+
/** Inputs whose values break a rule, their own or the form's, in form order. Inputs on the page may find more, such as text that is not a date yet. */
|
|
152
|
+
problems() {
|
|
153
|
+
return this.options.inputs.filter(params => !params.hidden && (this.ruleError(params.name) || this.crossErrors.has(params.name))).map(params => params.name);
|
|
154
|
+
}
|
|
155
|
+
/** Whether every value passes, as far as the values tell. `validate()` also asks the inputs on the page. */
|
|
156
|
+
isValid() {
|
|
157
|
+
return this.problems().length === 0 && !this.crossMessage;
|
|
158
|
+
}
|
|
159
|
+
/** Changes whenever the input must render again: a new value from code, or a message it shows. */
|
|
160
|
+
fieldVersion(name) {
|
|
161
|
+
return this.versions.get(name) ?? 0;
|
|
162
|
+
}
|
|
163
|
+
/** The input's render stamp: it mounts again when this changes, to show a value that did not come from typing. */
|
|
164
|
+
stamp(name) {
|
|
165
|
+
return `${this.generation}.${this.revisions.get(name) ?? 0}`;
|
|
166
|
+
}
|
|
167
|
+
/* Subscribing */
|
|
168
|
+
subscribeField(name, listener) {
|
|
169
|
+
let set = this.fieldListeners.get(name);
|
|
170
|
+
if (!set)
|
|
171
|
+
this.fieldListeners.set(name, (set = new Set()));
|
|
172
|
+
set.add(listener);
|
|
173
|
+
return () => {
|
|
174
|
+
set.delete(listener);
|
|
175
|
+
if (!set.size && this.fieldListeners.get(name) === set)
|
|
176
|
+
this.fieldListeners.delete(name);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
subscribeStatus = (listener) => {
|
|
180
|
+
this.statusListeners.add(listener);
|
|
181
|
+
return () => this.statusListeners.delete(listener);
|
|
182
|
+
};
|
|
183
|
+
/** Every value after each change. `name` is the input the user changed, or `null` for a load, a reset, or several at once. */
|
|
184
|
+
subscribeValue = (listener) => {
|
|
185
|
+
const stored = listener;
|
|
186
|
+
this.valueListeners.add(stored);
|
|
187
|
+
return () => this.valueListeners.delete(stored);
|
|
188
|
+
};
|
|
189
|
+
/* Changing */
|
|
190
|
+
/**
|
|
191
|
+
* New inputs or rules. `recheck` checks every value again (the form does after a change of inputs or settings);
|
|
192
|
+
* without it, only the callbacks are swapped, which a parent's render does every time.
|
|
193
|
+
*/
|
|
194
|
+
configure(options, recheck = true) {
|
|
195
|
+
const previous = this.options;
|
|
196
|
+
const draftMoved = options.draftKey !== previous.draftKey;
|
|
197
|
+
this.options = options;
|
|
198
|
+
if (recheck) {
|
|
199
|
+
const shown = this.shownSnapshot();
|
|
200
|
+
this.indexInputs();
|
|
201
|
+
if (this.isStarted)
|
|
202
|
+
this.adoptNewInputs(previous.inputs);
|
|
203
|
+
this.checkAllRules();
|
|
204
|
+
this.checkCross();
|
|
205
|
+
this.bumpShownChanges(shown);
|
|
206
|
+
this.patchStatus({ message: this.formMessage() });
|
|
207
|
+
}
|
|
208
|
+
if (draftMoved && this.isStarted) {
|
|
209
|
+
clearTimeout(this.draftTimer);
|
|
210
|
+
this.draftChecked = null;
|
|
211
|
+
if (options.draftKey)
|
|
212
|
+
void this.restoreDraft();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Loads values, such as a record from a server. They become the values the form compares with, and every input shows
|
|
217
|
+
* them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
|
|
218
|
+
* names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
|
|
219
|
+
* a saved draft for a later load (the form's placeholders show while the real values load). `quiet` tells no listener
|
|
220
|
+
* until `announce()`, for a load during a render.
|
|
221
|
+
*/
|
|
222
|
+
load(values = {}, { keepChanged = false, draft = true, quiet = false } = {}) {
|
|
223
|
+
this.quiet = quiet;
|
|
224
|
+
this.unannounced ||= quiet;
|
|
225
|
+
try {
|
|
226
|
+
return this.loadValues(values, keepChanged, draft);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
this.quiet = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Tells listeners about a quiet load. */
|
|
233
|
+
announce() {
|
|
234
|
+
if (!this.unannounced)
|
|
235
|
+
return;
|
|
236
|
+
this.unannounced = false;
|
|
237
|
+
for (const listener of [...this.statusListeners])
|
|
238
|
+
listener();
|
|
239
|
+
this.emit(null);
|
|
240
|
+
}
|
|
241
|
+
loadValues(values, keepChanged, draft) {
|
|
242
|
+
if (!this.inputByName.size && this.options.inputs.length)
|
|
243
|
+
this.indexInputs();
|
|
244
|
+
const next = this.normalize(values);
|
|
245
|
+
const merge = keepChanged && this.isStarted;
|
|
246
|
+
const conflicts = [];
|
|
247
|
+
let current = next;
|
|
248
|
+
if (merge) {
|
|
249
|
+
current = { ...next };
|
|
250
|
+
for (const name of this.changedInputs()) {
|
|
251
|
+
if (!sameData(next[name], this.initial[name]))
|
|
252
|
+
conflicts.push(name);
|
|
253
|
+
current[name] = this.values[name];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const previous = this.values;
|
|
257
|
+
this.initial = next;
|
|
258
|
+
this.values = current;
|
|
259
|
+
this.changedNames = new Set(merge ? [...this.changedNames].filter(name => !inputValuesEqual(current[name], next[name])) : []);
|
|
260
|
+
if (merge) {
|
|
261
|
+
// Only inputs whose value changed mount again, so an input being typed in keeps its focus.
|
|
262
|
+
for (const params of this.options.inputs)
|
|
263
|
+
if (!inputValuesEqual(previous[params.name], current[params.name]))
|
|
264
|
+
this.nextRevision(params.name);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.generation++;
|
|
268
|
+
this.touched.clear();
|
|
269
|
+
}
|
|
270
|
+
this.serverErrors.clear();
|
|
271
|
+
this.serverMessage = null;
|
|
272
|
+
this.forced.clear();
|
|
273
|
+
this.showAll = false;
|
|
274
|
+
this.attention = [];
|
|
275
|
+
this.draftDropped = false;
|
|
276
|
+
this.checkAllRules();
|
|
277
|
+
this.checkCross();
|
|
278
|
+
this.isStarted = true;
|
|
279
|
+
this.bumpAll();
|
|
280
|
+
this.patchStatus({
|
|
281
|
+
changed: this.changedNames.size > 0,
|
|
282
|
+
showAll: false,
|
|
283
|
+
problems: [],
|
|
284
|
+
message: null,
|
|
285
|
+
savedAt: null,
|
|
286
|
+
restored: merge ? this.status.restored : null,
|
|
287
|
+
});
|
|
288
|
+
this.emit(null);
|
|
289
|
+
const { draftKey } = this.options;
|
|
290
|
+
if (draft && draftKey && this.draftChecked !== draftKey)
|
|
291
|
+
void this.restoreDraft();
|
|
292
|
+
else
|
|
293
|
+
this.scheduleDraft();
|
|
294
|
+
return conflicts;
|
|
295
|
+
}
|
|
296
|
+
/** Brings back the values the form opened with or saved last, and clears every message. The draft goes too. */
|
|
297
|
+
reset() {
|
|
298
|
+
this.values = { ...this.initial };
|
|
299
|
+
this.changedNames.clear();
|
|
300
|
+
this.generation++;
|
|
301
|
+
this.clearMessages();
|
|
302
|
+
this.checkAllRules();
|
|
303
|
+
this.checkCross();
|
|
304
|
+
this.bumpAll();
|
|
305
|
+
this.patchStatus({ changed: false, showAll: false, problems: [], message: null, restored: null });
|
|
306
|
+
this.emit(null);
|
|
307
|
+
this.flushDraft();
|
|
308
|
+
}
|
|
309
|
+
/** Sets a value from code. Its input shows it at once, and it counts as a change. */
|
|
310
|
+
set(name, value) {
|
|
311
|
+
this.change([[name, value]], "code");
|
|
312
|
+
}
|
|
313
|
+
/** Sets several values from code, as one change. */
|
|
314
|
+
setValues(values) {
|
|
315
|
+
this.change(Object.entries(values), "code");
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A value the user typed or picked, from the input rendered with `stamp`. An input that was replaced since (by a value
|
|
319
|
+
* from code, a load, or a reset) can still report its last value as it leaves; that value is ignored.
|
|
320
|
+
*/
|
|
321
|
+
input(name, value, stamp) {
|
|
322
|
+
if (stamp !== this.stamp(name))
|
|
323
|
+
return;
|
|
324
|
+
this.change([[name, value]], "input");
|
|
325
|
+
}
|
|
326
|
+
/** Marks an input as left by the user: the form's rule messages on it show from now on. */
|
|
327
|
+
touch(name) {
|
|
328
|
+
if (this.touched.has(name))
|
|
329
|
+
return;
|
|
330
|
+
const before = this.shownError(name);
|
|
331
|
+
this.touched.add(name);
|
|
332
|
+
if (this.shownError(name) !== before)
|
|
333
|
+
this.bump(name);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Shows messages from outside, such as a server's, on their inputs, with a message for the whole form. They replace
|
|
337
|
+
* the ones shown before; an input's message clears once its value changes. Names that are not inputs join the form's message.
|
|
338
|
+
*/
|
|
339
|
+
setErrors(errors = {}, message) {
|
|
340
|
+
const shown = this.shownSnapshot();
|
|
341
|
+
const others = [];
|
|
342
|
+
this.serverErrors.clear();
|
|
343
|
+
for (const [name, text] of Object.entries(errors)) {
|
|
344
|
+
if (!text)
|
|
345
|
+
continue;
|
|
346
|
+
const params = this.inputByName.get(name);
|
|
347
|
+
if (params && !params.hidden)
|
|
348
|
+
this.serverErrors.set(name, text);
|
|
349
|
+
else
|
|
350
|
+
others.push(text);
|
|
351
|
+
}
|
|
352
|
+
this.serverMessage = [message, ...others].filter(Boolean).join(" ") || null;
|
|
353
|
+
this.bumpShownChanges(shown);
|
|
354
|
+
this.patchStatus({ message: this.formMessage(), problems: this.problemList() });
|
|
355
|
+
}
|
|
356
|
+
/** Clears the messages set with `setErrors`. */
|
|
357
|
+
clearErrors() {
|
|
358
|
+
this.setErrors({}, null);
|
|
359
|
+
}
|
|
360
|
+
/* The page */
|
|
361
|
+
/** Checks every value as a submit does: every message shows, and the first problem takes focus. Returns whether all is valid. */
|
|
362
|
+
validate() {
|
|
363
|
+
const problems = this.view ? this.view.validate() : this.reveal(this.problems());
|
|
364
|
+
return problems.length === 0 && !this.crossMessage;
|
|
365
|
+
}
|
|
366
|
+
/** Focuses an input by name, or the form's first input. */
|
|
367
|
+
focus(name) {
|
|
368
|
+
this.view?.focus(name);
|
|
369
|
+
}
|
|
370
|
+
/** Submits the form as its Save button does. Only a `SubmitForm` or an `ApiForm` can. */
|
|
371
|
+
submit() {
|
|
372
|
+
this.submitter?.();
|
|
373
|
+
}
|
|
374
|
+
/* Drafts */
|
|
375
|
+
/** Writes the draft now, e.g. when the page is hidden. With nothing changed, the draft is removed. */
|
|
376
|
+
flushDraft() {
|
|
377
|
+
clearTimeout(this.draftTimer);
|
|
378
|
+
this.draftTimer = undefined;
|
|
379
|
+
const key = this.options.draftKey;
|
|
380
|
+
if (!key || this.draftChecked !== key || this.draftDropped)
|
|
381
|
+
return;
|
|
382
|
+
const seq = ++this.draftSeq;
|
|
383
|
+
if (!this.changedNames.size) {
|
|
384
|
+
void deleteFormDraft(key);
|
|
385
|
+
this.patchStatus({ draftSavedAt: null });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const values = {};
|
|
389
|
+
const base = {};
|
|
390
|
+
for (const name of this.changedNames) {
|
|
391
|
+
values[name] = this.values[name];
|
|
392
|
+
base[name] = this.initial[name];
|
|
393
|
+
}
|
|
394
|
+
const savedAt = Date.now();
|
|
395
|
+
void writeFormDraft(key, { values, base, savedAt }).then(stored => {
|
|
396
|
+
if (stored && seq === this.draftSeq && key === this.options.draftKey && this.changedNames.size)
|
|
397
|
+
this.patchStatus({ draftSavedAt: savedAt });
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/** Puts back the values of the draft brought back when the form opened, and removes the draft. */
|
|
401
|
+
discardDraft() {
|
|
402
|
+
const restored = this.status.restored;
|
|
403
|
+
this.patchStatus({ restored: null });
|
|
404
|
+
if (restored)
|
|
405
|
+
this.change(restored.names.map(name => [name, this.initial[name]]), "code");
|
|
406
|
+
this.flushDraft();
|
|
407
|
+
}
|
|
408
|
+
/** Hides the notice of a restored draft and keeps its values. */
|
|
409
|
+
dismissDraft() {
|
|
410
|
+
this.patchStatus({ restored: null });
|
|
411
|
+
}
|
|
412
|
+
/** Removes the draft and leaves the values as they are, e.g. when the user cancels. Drafts start again with the next change. */
|
|
413
|
+
dropDraft() {
|
|
414
|
+
clearTimeout(this.draftTimer);
|
|
415
|
+
this.draftSeq++;
|
|
416
|
+
this.draftDropped = true;
|
|
417
|
+
const key = this.options.draftKey;
|
|
418
|
+
if (key)
|
|
419
|
+
void deleteFormDraft(key);
|
|
420
|
+
this.patchStatus({ draftSavedAt: null, restored: null });
|
|
421
|
+
}
|
|
422
|
+
/* For the form's components */
|
|
423
|
+
/** @internal Shows every message, as a submit does. `forced` are inputs whose own messages missed a problem their values have. */
|
|
424
|
+
reveal(problems, forced = []) {
|
|
425
|
+
const shown = this.shownSnapshot();
|
|
426
|
+
this.showAll = true;
|
|
427
|
+
for (const name of forced)
|
|
428
|
+
this.forced.add(name);
|
|
429
|
+
this.attention = problems;
|
|
430
|
+
this.bumpShownChanges(shown);
|
|
431
|
+
this.patchStatus({ showAll: true, problems: this.problemList(), message: this.formMessage() });
|
|
432
|
+
return problems;
|
|
433
|
+
}
|
|
434
|
+
/** @internal The inputs the page finds problems in, as values change after a submit. */
|
|
435
|
+
setAttention(names) {
|
|
436
|
+
if (sameList(names, this.attention))
|
|
437
|
+
return;
|
|
438
|
+
this.attention = names;
|
|
439
|
+
this.patchStatus({ problems: this.problemList() });
|
|
440
|
+
}
|
|
441
|
+
/** @internal A submit starts: messages from the last answer go. */
|
|
442
|
+
beginSubmit() {
|
|
443
|
+
const shown = this.shownSnapshot();
|
|
444
|
+
this.serverErrors.clear();
|
|
445
|
+
this.serverMessage = null;
|
|
446
|
+
this.bumpShownChanges(shown);
|
|
447
|
+
this.patchStatus({ submitting: true, message: this.formMessage(), problems: this.problemList() });
|
|
448
|
+
}
|
|
449
|
+
/** @internal A submit ended without a save. */
|
|
450
|
+
endSubmit() {
|
|
451
|
+
this.patchStatus({ submitting: false });
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* @internal A save: the values sent become the ones the form compares with; values typed while it ran stay changes.
|
|
455
|
+
* `extra` (from the server, such as a new id) is set on top.
|
|
456
|
+
*/
|
|
457
|
+
commit(saved, extra) {
|
|
458
|
+
const shown = this.shownSnapshot();
|
|
459
|
+
this.initial = extra ? this.normalize({ ...saved, ...extra }) : saved;
|
|
460
|
+
if (extra) {
|
|
461
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
462
|
+
const params = this.inputByName.get(name);
|
|
463
|
+
const next = params && !params.hidden ? normalizeInputValue(params, value) : value;
|
|
464
|
+
if (inputValuesEqual(this.values[name], next))
|
|
465
|
+
continue;
|
|
466
|
+
this.values = { ...this.values, [name]: next };
|
|
467
|
+
if (params)
|
|
468
|
+
this.nextRevision(name);
|
|
469
|
+
this.bump(name);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.changedNames = new Set(this.options.inputs.filter(params => !inputValuesEqual(this.values[params.name], this.initial[params.name])).map(params => params.name));
|
|
473
|
+
this.clearMessages();
|
|
474
|
+
this.bumpShownChanges(shown);
|
|
475
|
+
this.patchStatus({ changed: this.changedNames.size > 0, submitting: false, showAll: false, problems: [], message: null, savedAt: Date.now(), restored: null });
|
|
476
|
+
if (extra)
|
|
477
|
+
this.emit(null);
|
|
478
|
+
this.flushDraft();
|
|
479
|
+
}
|
|
480
|
+
/** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
|
|
481
|
+
markSaved() {
|
|
482
|
+
this.patchStatus({ submitting: false, savedAt: Date.now() });
|
|
483
|
+
}
|
|
484
|
+
/** @internal */
|
|
485
|
+
setLoading(loading) {
|
|
486
|
+
this.patchStatus({ loading });
|
|
487
|
+
}
|
|
488
|
+
/** @internal */
|
|
489
|
+
setFormId(formId) {
|
|
490
|
+
this.patchStatus({ formId });
|
|
491
|
+
}
|
|
492
|
+
/** @internal A `FormButtons` shows the status; returns its removal. */
|
|
493
|
+
addButtons() {
|
|
494
|
+
this.patchStatus({ buttons: this.status.buttons + 1 });
|
|
495
|
+
return () => this.patchStatus({ buttons: Math.max(0, this.status.buttons - 1) });
|
|
496
|
+
}
|
|
497
|
+
/* Inside */
|
|
498
|
+
indexInputs() {
|
|
499
|
+
this.inputByName = new Map(this.options.inputs.map(params => [params.name, params]));
|
|
500
|
+
}
|
|
501
|
+
/** Each input's value in the type the input reports; a missing one is the input's default. Other fields stay as they came. */
|
|
502
|
+
normalize(values) {
|
|
503
|
+
const result = { ...values };
|
|
504
|
+
for (const params of this.options.inputs) {
|
|
505
|
+
// A hidden input's value stays in the data as it was.
|
|
506
|
+
if (params.hidden)
|
|
507
|
+
continue;
|
|
508
|
+
result[params.name] = params.name in values ? normalizeInputValue(params, values[params.name]) : initialInputValue(params);
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
/** Inputs that were not there, or were hidden, get their value in the input's type, as a load gives it. */
|
|
513
|
+
adoptNewInputs(previous) {
|
|
514
|
+
const before = new Map(previous.map(params => [params.name, params]));
|
|
515
|
+
for (const params of this.options.inputs) {
|
|
516
|
+
const old = before.get(params.name);
|
|
517
|
+
if (params.hidden || (old && !old.hidden))
|
|
518
|
+
continue;
|
|
519
|
+
const { name } = params;
|
|
520
|
+
const value = name in this.values ? normalizeInputValue(params, this.values[name]) : initialInputValue(params);
|
|
521
|
+
const initial = name in this.initial ? normalizeInputValue(params, this.initial[name]) : value;
|
|
522
|
+
this.values = { ...this.values, [name]: value };
|
|
523
|
+
this.initial = { ...this.initial, [name]: initial };
|
|
524
|
+
if (inputValuesEqual(value, initial))
|
|
525
|
+
this.changedNames.delete(name);
|
|
526
|
+
else
|
|
527
|
+
this.changedNames.add(name);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
change(entries, source) {
|
|
531
|
+
const crossBefore = this.crossErrors;
|
|
532
|
+
const shown = new Map();
|
|
533
|
+
const remounted = new Set();
|
|
534
|
+
const written = [];
|
|
535
|
+
const remember = (name) => {
|
|
536
|
+
if (!shown.has(name))
|
|
537
|
+
shown.set(name, this.shownError(name));
|
|
538
|
+
};
|
|
539
|
+
const write = (name, value, from) => {
|
|
540
|
+
const params = this.inputByName.get(name);
|
|
541
|
+
const next = params && !params.hidden && from === "code" ? normalizeInputValue(params, value) : value;
|
|
542
|
+
if (inputValuesEqual(this.values[name], next))
|
|
543
|
+
return;
|
|
544
|
+
remember(name);
|
|
545
|
+
this.values = { ...this.values, [name]: next };
|
|
546
|
+
if (from === "code" && params) {
|
|
547
|
+
this.nextRevision(name);
|
|
548
|
+
remounted.add(name);
|
|
549
|
+
}
|
|
550
|
+
// A changed value is a new question: the server's answer and the form's shown rule message no longer apply.
|
|
551
|
+
this.serverErrors.delete(name);
|
|
552
|
+
this.forced.delete(name);
|
|
553
|
+
this.checkRule(name);
|
|
554
|
+
if (inputValuesEqual(next, this.initial[name]))
|
|
555
|
+
this.changedNames.delete(name);
|
|
556
|
+
else
|
|
557
|
+
this.changedNames.add(name);
|
|
558
|
+
written.push(name);
|
|
559
|
+
};
|
|
560
|
+
for (const [name, value] of entries)
|
|
561
|
+
write(name, value, source);
|
|
562
|
+
if (!written.length)
|
|
563
|
+
return;
|
|
564
|
+
// Values derived from the ones the user changed, such as an amount from a quantity and a rate.
|
|
565
|
+
const { onFieldChange } = this.options;
|
|
566
|
+
if (source === "input" && onFieldChange) {
|
|
567
|
+
for (const name of [...written]) {
|
|
568
|
+
const patch = onFieldChange(name, this.values[name], this.values);
|
|
569
|
+
if (patch)
|
|
570
|
+
for (const [field, value] of Object.entries(patch))
|
|
571
|
+
if (field !== name)
|
|
572
|
+
write(field, value, "code");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// The form's rule runs again: an input it now names, or no longer names, shows the change even if nobody typed in it.
|
|
576
|
+
for (const name of this.checkCross())
|
|
577
|
+
if (!shown.has(name))
|
|
578
|
+
shown.set(name, this.shownWith(name, crossBefore));
|
|
579
|
+
for (const [name, before] of shown)
|
|
580
|
+
if (remounted.has(name) || this.shownError(name) !== before)
|
|
581
|
+
this.bump(name);
|
|
582
|
+
this.patchStatus({ changed: this.changedNames.size > 0, message: this.formMessage(), problems: this.problemList() });
|
|
583
|
+
this.emit(source === "input" ? written[0] : written.length === 1 ? written[0] : null);
|
|
584
|
+
// A change after a cancel is worth keeping again.
|
|
585
|
+
if (source === "input")
|
|
586
|
+
this.draftDropped = false;
|
|
587
|
+
this.scheduleDraft();
|
|
588
|
+
}
|
|
589
|
+
checkRule(name) {
|
|
590
|
+
const params = this.inputByName.get(name);
|
|
591
|
+
if (params && isRowsInput(params)) {
|
|
592
|
+
this.staleRules.add(name);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this.staleRules.delete(name);
|
|
596
|
+
const message = params && !params.hidden && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
|
|
597
|
+
if (message)
|
|
598
|
+
this.ruleErrors.set(name, message);
|
|
599
|
+
else
|
|
600
|
+
this.ruleErrors.delete(name);
|
|
601
|
+
}
|
|
602
|
+
checkAllRules() {
|
|
603
|
+
this.ruleErrors.clear();
|
|
604
|
+
this.staleRules.clear();
|
|
605
|
+
for (const params of this.options.inputs)
|
|
606
|
+
this.checkRule(params.name);
|
|
607
|
+
}
|
|
608
|
+
/** Runs the form's own rule; returns the inputs whose message from it changed. */
|
|
609
|
+
checkCross() {
|
|
610
|
+
const result = this.options.locked ? null : this.options.validate?.(this.values);
|
|
611
|
+
const next = new Map();
|
|
612
|
+
const others = [];
|
|
613
|
+
if (typeof result === "string") {
|
|
614
|
+
if (result)
|
|
615
|
+
others.push(result);
|
|
616
|
+
}
|
|
617
|
+
else if (result && typeof result === "object") {
|
|
618
|
+
for (const [name, message] of Object.entries(result)) {
|
|
619
|
+
if (!message)
|
|
620
|
+
continue;
|
|
621
|
+
const params = this.inputByName.get(name);
|
|
622
|
+
if (params && !params.hidden)
|
|
623
|
+
next.set(name, message);
|
|
624
|
+
else
|
|
625
|
+
others.push(message);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const changed = [];
|
|
629
|
+
for (const name of new Set([...this.crossErrors.keys(), ...next.keys()]))
|
|
630
|
+
if (this.crossErrors.get(name) !== next.get(name))
|
|
631
|
+
changed.push(name);
|
|
632
|
+
this.crossErrors = next;
|
|
633
|
+
this.crossMessage = others.length ? others.join(" ") : null;
|
|
634
|
+
return changed;
|
|
635
|
+
}
|
|
636
|
+
clearMessages() {
|
|
637
|
+
this.serverErrors.clear();
|
|
638
|
+
this.serverMessage = null;
|
|
639
|
+
this.forced.clear();
|
|
640
|
+
this.touched.clear();
|
|
641
|
+
this.showAll = false;
|
|
642
|
+
this.attention = [];
|
|
643
|
+
}
|
|
644
|
+
/** The form's message: a server's, or the form's own rule once a submit ran. */
|
|
645
|
+
formMessage() {
|
|
646
|
+
if (this.serverMessage)
|
|
647
|
+
return this.serverMessage;
|
|
648
|
+
return this.showAll ? this.crossMessage : null;
|
|
649
|
+
}
|
|
650
|
+
/** Inputs to point at: the ones the page found on the last submit, and those with a server's message, in form order. */
|
|
651
|
+
problemList() {
|
|
652
|
+
if (!this.attention.length && !this.serverErrors.size)
|
|
653
|
+
return this.status.problems.length ? [] : this.status.problems;
|
|
654
|
+
const names = this.options.inputs.filter(params => this.attention.includes(params.name) || this.serverErrors.has(params.name)).map(params => params.name);
|
|
655
|
+
return sameList(names, this.status.problems) ? this.status.problems : names;
|
|
656
|
+
}
|
|
657
|
+
nextRevision(name) {
|
|
658
|
+
this.revisions.set(name, (this.revisions.get(name) ?? 0) + 1);
|
|
659
|
+
}
|
|
660
|
+
bump(name) {
|
|
661
|
+
this.versions.set(name, (this.versions.get(name) ?? 0) + 1);
|
|
662
|
+
const listeners = this.fieldListeners.get(name);
|
|
663
|
+
if (listeners && !this.quiet)
|
|
664
|
+
for (const listener of [...listeners])
|
|
665
|
+
listener();
|
|
666
|
+
}
|
|
667
|
+
bumpAll() {
|
|
668
|
+
for (const params of this.options.inputs)
|
|
669
|
+
this.bump(params.name);
|
|
670
|
+
}
|
|
671
|
+
shownSnapshot() {
|
|
672
|
+
return new Map(this.options.inputs.map(params => [params.name, this.shownError(params.name)]));
|
|
673
|
+
}
|
|
674
|
+
bumpShownChanges(before) {
|
|
675
|
+
for (const params of this.options.inputs)
|
|
676
|
+
if (this.shownError(params.name) !== before.get(params.name))
|
|
677
|
+
this.bump(params.name);
|
|
678
|
+
}
|
|
679
|
+
patchStatus(patch) {
|
|
680
|
+
const current = this.status;
|
|
681
|
+
if (!Object.entries(patch).some(([key, value]) => !Object.is(value, current[key])))
|
|
682
|
+
return;
|
|
683
|
+
this.status = { ...this.status, ...patch };
|
|
684
|
+
if (!this.quiet)
|
|
685
|
+
for (const listener of [...this.statusListeners])
|
|
686
|
+
listener();
|
|
687
|
+
}
|
|
688
|
+
emit(name) {
|
|
689
|
+
if (this.quiet)
|
|
690
|
+
return;
|
|
691
|
+
for (const listener of [...this.valueListeners])
|
|
692
|
+
listener(this.values, name);
|
|
693
|
+
}
|
|
694
|
+
scheduleDraft() {
|
|
695
|
+
const key = this.options.draftKey;
|
|
696
|
+
if (!key || this.draftChecked !== key)
|
|
697
|
+
return;
|
|
698
|
+
clearTimeout(this.draftTimer);
|
|
699
|
+
this.draftTimer = setTimeout(() => this.flushDraft(), DRAFT_DELAY);
|
|
700
|
+
}
|
|
701
|
+
/** Looks for a draft under the form's key and brings back its values, except those the user changed since the form opened. */
|
|
702
|
+
async restoreDraft() {
|
|
703
|
+
const key = this.options.draftKey;
|
|
704
|
+
if (!key)
|
|
705
|
+
return;
|
|
706
|
+
const draft = await readFormDraft(key);
|
|
707
|
+
if (key !== this.options.draftKey || this.draftChecked === key)
|
|
708
|
+
return;
|
|
709
|
+
this.draftChecked = key;
|
|
710
|
+
if (!draft) {
|
|
711
|
+
this.scheduleDraft();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const entries = [];
|
|
715
|
+
const conflicts = [];
|
|
716
|
+
for (const params of this.options.inputs) {
|
|
717
|
+
const { name } = params;
|
|
718
|
+
if (params.hidden || !(name in draft.values) || this.changedNames.has(name))
|
|
719
|
+
continue;
|
|
720
|
+
const value = draft.values[name];
|
|
721
|
+
if (sameData(value, this.values[name]))
|
|
722
|
+
continue;
|
|
723
|
+
if (name in draft.base && !sameData(draft.base[name], this.initial[name]))
|
|
724
|
+
conflicts.push(name);
|
|
725
|
+
entries.push([name, value]);
|
|
726
|
+
}
|
|
727
|
+
if (!entries.length) {
|
|
728
|
+
this.flushDraft();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
this.change(entries, "code");
|
|
732
|
+
this.patchStatus({ restored: { savedAt: draft.savedAt, names: entries.map(([name]) => name), conflicts }, draftSavedAt: draft.savedAt });
|
|
733
|
+
}
|
|
734
|
+
}
|