nexus-shared 3.1.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.
Files changed (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -1,5 +1,7 @@
1
1
  import { deleteFormDraft, readFormDraft, writeFormDraft } from "./FormDrafts.js";
2
- import { initialInputValue, inputValuesEqual, isRowsInput, normalizeInputValue, validateInput } from "./InputParamsHelpers.js";
2
+ import { isComputedInput } from "./InputConditions.js";
3
+ import { findRememberedOpening, forgetInputMemory, isEmptyInputValue, isPickerInput, readInputMemory, rememberDependencies, rememberedOpeningNow, writeInputMemory } from "./InputMemory.js";
4
+ import { holdsValue, initialInputValue, inputValuesEqual, isRowsInput, keepsTyping, normalizeInputValue, validateInput } from "./InputParamsHelpers.js";
3
5
  const INITIAL_STATUS = {
4
6
  changed: false,
5
7
  submitting: false,
@@ -40,6 +42,21 @@ export class FormStore {
40
42
  inputByName = new Map();
41
43
  values = {};
42
44
  initial = {};
45
+ /** The values the last load was given, as given: an input they name with a value is the form's own, never remembered. */
46
+ given = {};
47
+ /** Where remembered inputs (`remember`) are read and kept; set by the form that draws this store, in the browser. */
48
+ memory = null;
49
+ /** Pickers asked whether they still offer a remembered value; the next opening (a load, a reset) or a submit stops it. */
50
+ memoryRound = null;
51
+ /** The inputs whose picker is being asked: drawn loading, and a change to one (the person's pick, code) ends its ask. */
52
+ asking = new Set();
53
+ /** The values memory opened inputs on: an opening that finds nothing (a reset after the choice went) takes its own back. */
54
+ fromMemory = new Map();
55
+ /**
56
+ * The values a load (or a reset, a draft) is working out, while it works them out: what `get` answers then, so an
57
+ * input's live `hidden` (`showWhen`) is judged on the values the form opens with, never on the ones it had before.
58
+ */
59
+ reading = null;
43
60
  isStarted = false;
44
61
  // Messages: an input's own rules, the form's rule, a server's, and rule messages the form shows for its inputs.
45
62
  ruleErrors = new Map();
@@ -72,6 +89,8 @@ export class FormStore {
72
89
  draftDropped = false;
73
90
  /** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
74
91
  refusedWhileLoading = false;
92
+ /** What a choice input reported beside the value the user picked last (`pickedItem`): its item, kept with that value. */
93
+ picked = new Map();
75
94
  /** A load during a render tells nobody; `announce` does, once the form has mounted. */
76
95
  quiet = false;
77
96
  unannounced = false;
@@ -92,7 +111,13 @@ export class FormStore {
92
111
  return this.values;
93
112
  }
94
113
  get(name) {
95
- return this.values[name];
114
+ return this.reading ? this.reading[name] : this.values[name];
115
+ }
116
+ /** The params of the input of this name, as the form was given them; undefined for a name no input has. */
117
+ inputParams(name) {
118
+ if (!this.inputByName.size && this.options.inputs.length)
119
+ this.indexInputs();
120
+ return this.inputByName.get(name);
96
121
  }
97
122
  /** The values the form opened with, or saved last. */
98
123
  getInitial() {
@@ -112,6 +137,19 @@ export class FormStore {
112
137
  isChanged() {
113
138
  return this.changedNames.size > 0;
114
139
  }
140
+ /**
141
+ * Whether the changes are worth asking about before they are dropped (Cancel, closing a popup, leaving the page): a
142
+ * changed value of any input but a password or a one-time code (`keepsTyping`). A form where only those were typed is
143
+ * changed, and Save and its line say so, but nothing asks before it goes.
144
+ */
145
+ isWorthKeeping() {
146
+ for (const name of this.changedNames) {
147
+ const params = this.inputParams(name);
148
+ if (!params || keepsTyping(params))
149
+ return true;
150
+ }
151
+ return false;
152
+ }
115
153
  getStatus = () => this.status;
116
154
  /** 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. */
117
155
  shownError(name) {
@@ -129,12 +167,21 @@ export class FormStore {
129
167
  return this.ruleError(name) ?? undefined;
130
168
  return undefined;
131
169
  }
170
+ /**
171
+ * The item a choice input reported with the value it holds now, when the user picked that value: a dropdown's option
172
+ * (its options, with `multiple`). `undefined` for a value loaded, set from code, or changed since - so what it answers
173
+ * always goes with the value. A CRUD names what a save joined from it (`crudJoinedNames`).
174
+ */
175
+ pickedItem(name) {
176
+ const picked = this.picked.get(name);
177
+ return picked && inputValuesEqual(picked.value, this.values[name]) ? picked.item : undefined;
178
+ }
132
179
  /** The input's own rule message for its value, whether it shows or not. */
133
180
  ruleError(name) {
134
181
  if (this.staleRules.has(name)) {
135
182
  this.staleRules.delete(name);
136
183
  const params = this.inputByName.get(name);
137
- const message = params && !params.hidden && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
184
+ const message = params && !params.hidden && !isComputedInput(params) && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
138
185
  if (message)
139
186
  this.ruleErrors.set(name, message);
140
187
  else
@@ -152,7 +199,7 @@ export class FormStore {
152
199
  }
153
200
  /** 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. */
154
201
  problems() {
155
- return this.options.inputs.filter(params => !params.hidden && (this.ruleError(params.name) || this.crossErrors.has(params.name))).map(params => params.name);
202
+ return this.options.inputs.filter(params => !params.hidden && !isComputedInput(params) && (this.ruleError(params.name) || this.crossErrors.has(params.name))).map(params => params.name);
156
203
  }
157
204
  /** Whether every value passes, as far as the values tell. `validate()` also asks the inputs on the page. */
158
205
  isValid() {
@@ -247,7 +294,10 @@ export class FormStore {
247
294
  loadValues(values, keepChanged, draft) {
248
295
  if (!this.inputByName.size && this.options.inputs.length)
249
296
  this.indexInputs();
250
- const next = this.normalize(values);
297
+ this.given = values;
298
+ // A load opens afresh: what memory opened before belongs to the values it replaces.
299
+ this.fromMemory.clear();
300
+ const next = this.openRemembered(this.normalize(values), values);
251
301
  const merge = keepChanged && this.isStarted;
252
302
  const conflicts = [];
253
303
  let current = next;
@@ -301,8 +351,12 @@ export class FormStore {
301
351
  this.scheduleDraft();
302
352
  return conflicts;
303
353
  }
304
- /** Brings back the values the form opened with or saved last, and clears every message. The draft goes too. */
354
+ /**
355
+ * Brings back the values the form opened with or saved last, and clears every message. The draft goes too. A
356
+ * remembered input opens on the choice kept now, so a form reset after a save starts on what was just saved.
357
+ */
305
358
  reset() {
359
+ this.initial = this.openRemembered({ ...this.initial }, this.given);
306
360
  this.values = { ...this.initial };
307
361
  this.changedNames.clear();
308
362
  this.generation++;
@@ -323,10 +377,11 @@ export class FormStore {
323
377
  this.change(Object.entries(values), "code");
324
378
  }
325
379
  /**
326
- * A value the user typed or picked, from the input rendered with `stamp`. An input that was replaced since (by a value
327
- * from code, a load, or a reset) can still report its last value as it leaves; that value is ignored.
380
+ * A value the user typed or picked, from the input rendered with `stamp`, with what a choice input reported beside it
381
+ * (`item`: a dropdown's option, kept for `pickedItem`). An input that was replaced since (by a value from code, a load,
382
+ * or a reset) can still report its last value as it leaves; that value is ignored.
328
383
  */
329
- input(name, value, stamp) {
384
+ input(name, value, stamp, item) {
330
385
  if (stamp !== this.stamp(name))
331
386
  return;
332
387
  // Nothing is taken while the values load, not even from an input whose `disabled` was taken off by hand: the
@@ -335,6 +390,10 @@ export class FormStore {
335
390
  this.refusedWhileLoading = true;
336
391
  return;
337
392
  }
393
+ if (item === undefined)
394
+ this.picked.delete(name);
395
+ else
396
+ this.picked.set(name, { value, item });
338
397
  this.change([[name, value]], "input");
339
398
  }
340
399
  /** Marks an input as left by the user: the form's rule messages on it show from now on. */
@@ -358,7 +417,8 @@ export class FormStore {
358
417
  if (!text)
359
418
  continue;
360
419
  const params = this.inputByName.get(name);
361
- if (params && !params.hidden)
420
+ // A worked-out input is never typed in, so a message naming it is the form's.
421
+ if (params && !params.hidden && !isComputedInput(params))
362
422
  this.serverErrors.set(name, text);
363
423
  else
364
424
  others.push(text);
@@ -454,6 +514,8 @@ export class FormStore {
454
514
  }
455
515
  /** @internal A submit starts: messages from the last answer go. */
456
516
  beginSubmit() {
517
+ // What is in the form is what is saved: a remembered value a picker answers for now would come in after it.
518
+ this.stopAskingPickers();
457
519
  const shown = this.shownSnapshot();
458
520
  this.serverErrors.clear();
459
521
  this.serverMessage = null;
@@ -474,6 +536,8 @@ export class FormStore {
474
536
  if (extra) {
475
537
  for (const [name, value] of Object.entries(extra)) {
476
538
  const params = this.inputByName.get(name);
539
+ if (isComputedInput(params))
540
+ continue;
477
541
  const next = params && !params.hidden ? normalizeInputValue(params, value) : value;
478
542
  if (inputValuesEqual(this.values[name], next))
479
543
  continue;
@@ -491,6 +555,67 @@ export class FormStore {
491
555
  this.emit(null);
492
556
  this.flushDraft();
493
557
  }
558
+ /**
559
+ * @internal Whether the input's picker is being asked whether it still offers a remembered value: the input draws
560
+ * loading until it answers, the person picks, or the form opens again.
561
+ */
562
+ isRemembering(name) {
563
+ return this.asking.has(name);
564
+ }
565
+ /**
566
+ * @internal The browser storage remembered inputs read and keep (`InputLayoutParams.remember`), set by the form that
567
+ * draws this store once it is in the browser. Inputs the values left without one open on it at once (as the values
568
+ * the form opened with), a picker once it answers that it still offers the value; a form drawn on a server opened
569
+ * without it, so nothing differs while it hydrates.
570
+ */
571
+ attachMemory(storage) {
572
+ if (this.memory === storage)
573
+ return;
574
+ this.memory = storage;
575
+ if (!storage || !this.isStarted)
576
+ return;
577
+ const opened = this.openRemembered({ ...this.initial }, this.given);
578
+ const changed = [];
579
+ for (const params of this.options.inputs) {
580
+ const { name } = params;
581
+ // Only an input still as the form opened it: one typed in (or restored from a draft) keeps what it has.
582
+ if (!params.remember || isComputedInput(params) || this.changedNames.has(name) || inputValuesEqual(opened[name], this.initial[name]))
583
+ continue;
584
+ this.initial = { ...this.initial, [name]: opened[name] };
585
+ this.values = { ...this.values, [name]: opened[name] };
586
+ this.nextRevision(name);
587
+ this.bump(name);
588
+ changed.push(name);
589
+ }
590
+ if (!changed.length)
591
+ return;
592
+ this.checkAllRules();
593
+ this.checkCross();
594
+ this.bumpFollowers(changed);
595
+ this.emit(null);
596
+ }
597
+ /**
598
+ * Inputs that follow ones memory opened (`dependsOn`: a `compute.from`, `showWhen`, `spanWhen`, `labelWhen`, or a
599
+ * `narrowedBy`) draw again with their params read afresh, as after a change: a cell redraws only on its own bump.
600
+ */
601
+ bumpFollowers(names) {
602
+ for (const name of this.followersOf(names))
603
+ if (!names.includes(name))
604
+ this.bump(name);
605
+ }
606
+ /**
607
+ * @internal A save: each remembered input keeps the value saved (an empty one forgets), for the next form that asks -
608
+ * under the saved values' `per` value, and only where its `when` holds on them.
609
+ */
610
+ rememberSaved(values) {
611
+ if (!this.memory)
612
+ return;
613
+ const read = (name) => values[name];
614
+ for (const params of this.options.inputs) {
615
+ if (params.remember && !params.hidden && !isComputedInput(params))
616
+ writeInputMemory(this.memory, params, read(params.name), read);
617
+ }
618
+ }
494
619
  /** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
495
620
  markSaved() {
496
621
  this.patchStatus({ submitting: false, savedAt: Date.now() });
@@ -521,20 +646,160 @@ export class FormStore {
521
646
  /** Each input's value in the type the input reports; a missing one is the input's default. Other fields stay as they came. */
522
647
  normalize(values) {
523
648
  const result = { ...values };
524
- for (const params of this.options.inputs) {
525
- // A hidden input's value stays in the data as it was.
526
- if (params.hidden)
527
- continue;
649
+ // A worked-out input (`compute`) holds no value: a field the data has under its name is not the form's to send.
650
+ for (const params of this.options.inputs)
651
+ if (isComputedInput(params))
652
+ delete result[params.name];
653
+ const inputs = this.options.inputs.filter(params => holdsValue(params) && !isComputedInput(params));
654
+ // Which inputs are hidden is judged on these values: an input shown by another's value (`showWhen`) whose other input
655
+ // gets its value in this very load (its default, the record's). So every input is worked out as shown first...
656
+ for (const params of inputs)
528
657
  result[params.name] = params.name in values ? normalizeInputValue(params, values[params.name]) : initialInputValue(params);
658
+ // ...then a hidden input's value goes back to what the data held (a picture has none: the record keeps what it came with).
659
+ const hidden = this.readingFrom(result, () => inputs.filter(params => params.hidden));
660
+ for (const params of hidden) {
661
+ if (params.name in values)
662
+ result[params.name] = values[params.name];
663
+ else
664
+ delete result[params.name];
665
+ }
666
+ return result;
667
+ }
668
+ /** Runs `work` with `get` answering from these values (a load's, a reset's, a draft's), and gives back its answer. */
669
+ readingFrom(values, work) {
670
+ const before = this.reading;
671
+ this.reading = values;
672
+ try {
673
+ return work();
674
+ }
675
+ finally {
676
+ this.reading = before;
677
+ }
678
+ }
679
+ /**
680
+ * The values with each remembered input the given values leave without one opened on the company's preferred value
681
+ * or the person's last choice (`rememberedOpeningNow`), its `when` and `per` read among these values; without storage
682
+ * (on a server, before the form is drawn) only a preferred value applies. A kept choice no longer offered is forgotten.
683
+ * A picker that must be asked first opens nothing now: it is asked, and its answer opens the input later
684
+ * (`askPickers`) - the round any earlier opening started stops here. An input memory opened before (a reset after a
685
+ * save, the storage arriving) that now opens on nothing takes the value the given values give it back.
686
+ */
687
+ openRemembered(values, given) {
688
+ let result = values;
689
+ // The values as they open, in form order: a group opened on its kept value names the package kept for it.
690
+ const read = (name) => result[name];
691
+ const asks = [];
692
+ const set = (name, value) => {
693
+ if (result === values)
694
+ result = { ...values };
695
+ result[name] = value;
696
+ };
697
+ // Shown or hidden as the values being opened say, not as the store's previous ones do.
698
+ const shown = this.readingFrom(values, () => this.options.inputs.filter(params => params.remember && !params.hidden && holdsValue(params) && !isComputedInput(params)));
699
+ for (const params of shown) {
700
+ const { name } = params;
701
+ if (name in given && !isEmptyInputValue(params, given[name]))
702
+ continue;
703
+ // A picker is asked in the browser only, once the storage came: a server's draw never calls it or reads its cache.
704
+ if (!this.memory && isPickerInput(params))
705
+ continue;
706
+ const stored = this.memory ? readInputMemory(this.memory, params.remember, read) : undefined;
707
+ const opening = rememberedOpeningNow(params, stored, read);
708
+ if (opening.forget)
709
+ forgetInputMemory(this.memory, params.remember, read);
710
+ if (opening.ask)
711
+ asks.push({ params, stored });
712
+ if (opening.value !== undefined) {
713
+ const value = normalizeInputValue(params, opening.value);
714
+ set(name, value);
715
+ this.fromMemory.set(name, value);
716
+ }
717
+ else if (this.fromMemory.has(name)) {
718
+ // Still the value memory opened it on: the input's own comes back, so a choice no longer offered is not left in.
719
+ if (inputValuesEqual(result[name], this.fromMemory.get(name))) {
720
+ set(name, name in given ? normalizeInputValue(params, given[name]) : initialInputValue(params));
721
+ }
722
+ this.fromMemory.delete(name);
723
+ }
529
724
  }
725
+ this.askPickers(asks, read);
530
726
  return result;
531
727
  }
728
+ /** Stops asking pickers about remembered values: their answers no longer open anything, and their inputs stop loading. */
729
+ stopAskingPickers() {
730
+ this.memoryRound?.abort();
731
+ this.memoryRound = null;
732
+ const asked = [...this.asking];
733
+ this.asking.clear();
734
+ for (const name of asked)
735
+ this.bump(name);
736
+ }
737
+ /**
738
+ * Asks each picker whether it still offers the remembered value (`findRememberedOpening`), the input drawn loading
739
+ * meanwhile. An answer forgets a choice no longer offered, and opens an offered one (`openAsked`) only while the input
740
+ * is still asked about - no pick, no value from code since - and the values it was judged on (`rememberDependencies`:
741
+ * the group that narrows it, the one it is kept per, its `when`) are still the form's: a group changed meanwhile opens
742
+ * nothing.
743
+ */
744
+ askPickers(asks, read) {
745
+ this.stopAskingPickers();
746
+ if (!asks.length)
747
+ return;
748
+ const round = new AbortController();
749
+ this.memoryRound = round;
750
+ const memory = this.memory;
751
+ for (const { params, stored } of asks) {
752
+ const { name } = params;
753
+ const judgedOn = rememberDependencies(params).map(other => [other, read(other)]);
754
+ this.asking.add(name);
755
+ this.bump(name);
756
+ void findRememberedOpening(params, stored, read, round.signal).then(opening => {
757
+ if (round.signal.aborted)
758
+ return;
759
+ if (opening.forget)
760
+ forgetInputMemory(memory, params.remember, read);
761
+ if (!this.asking.delete(name))
762
+ return;
763
+ const still = judgedOn.every(([other, value]) => inputValuesEqual(this.values[other], value));
764
+ if (opening.value !== undefined && still)
765
+ this.openAsked(name, opening.value);
766
+ else
767
+ this.bump(name);
768
+ });
769
+ }
770
+ }
771
+ /**
772
+ * A picker answered for a remembered value: the input opens on it, as the value the form opened with (not a change) -
773
+ * only while it is as the form opened it and still on the form. One the person typed in, or a draft gave back, keeps
774
+ * what it has.
775
+ */
776
+ openAsked(name, value) {
777
+ const params = this.inputByName.get(name);
778
+ if (!params || params.hidden || !holdsValue(params) || isComputedInput(params) || this.changedNames.has(name)) {
779
+ this.bump(name);
780
+ return;
781
+ }
782
+ const next = normalizeInputValue(params, value);
783
+ this.fromMemory.set(name, next);
784
+ if (inputValuesEqual(this.values[name], next)) {
785
+ this.bump(name);
786
+ return;
787
+ }
788
+ this.initial = { ...this.initial, [name]: next };
789
+ this.values = { ...this.values, [name]: next };
790
+ this.nextRevision(name);
791
+ this.bump(name);
792
+ this.checkAllRules();
793
+ this.checkCross();
794
+ this.bumpFollowers([name]);
795
+ this.emit(null);
796
+ }
532
797
  /** Inputs that were not there, or were hidden, get their value in the input's type, as a load gives it. */
533
798
  adoptNewInputs(previous) {
534
799
  const before = new Map(previous.map(params => [params.name, params]));
535
800
  for (const params of this.options.inputs) {
536
801
  const old = before.get(params.name);
537
- if (params.hidden || (old && !old.hidden))
802
+ if (params.hidden || !holdsValue(params) || isComputedInput(params) || (old && !old.hidden))
538
803
  continue;
539
804
  const { name } = params;
540
805
  const value = name in this.values ? normalizeInputValue(params, this.values[name]) : initialInputValue(params);
@@ -552,15 +817,22 @@ export class FormStore {
552
817
  const shown = new Map();
553
818
  const remounted = new Set();
554
819
  const written = [];
820
+ /** Inputs a picker was asked about for a remembered value: this change is theirs now, and they stop loading. */
821
+ const unasked = [];
555
822
  const remember = (name) => {
556
823
  if (!shown.has(name))
557
824
  shown.set(name, this.shownError(name));
558
825
  };
559
826
  const write = (name, value, from) => {
560
827
  const params = this.inputByName.get(name);
828
+ // A worked-out input holds no value: one set from code (or a draft, an `onFieldChange` patch) is not taken.
829
+ if (isComputedInput(params))
830
+ return;
561
831
  const next = params && !params.hidden && from === "code" ? normalizeInputValue(params, value) : value;
562
832
  if (inputValuesEqual(this.values[name], next))
563
833
  return;
834
+ if (this.asking.delete(name))
835
+ unasked.push(name);
564
836
  remember(name);
565
837
  this.values = { ...this.values, [name]: next };
566
838
  if (from === "code" && params) {
@@ -592,6 +864,27 @@ export class FormStore {
592
864
  write(field, value, "code");
593
865
  }
594
866
  }
867
+ // An input whose options another narrows (`narrowedBy`) loses the value it picked among the other's old options when
868
+ // a person changes the other one; a value set from code (a load, a patch) is the code's to keep consistent. Down a
869
+ // chain too: a Country change clears the Region, and with it the District picked among that Region's (each input
870
+ // once, whatever order the form lists them in; one the person set in this same change keeps its value).
871
+ if (source === "input") {
872
+ const changed = new Set(written);
873
+ let cleared = true;
874
+ while (cleared) {
875
+ cleared = false;
876
+ for (const params of this.options.inputs) {
877
+ const other = params.narrowedBy?.name;
878
+ if (!other || !changed.has(other) || changed.has(params.name))
879
+ continue;
880
+ changed.add(params.name);
881
+ cleared = true;
882
+ const kept = this.values[params.name];
883
+ if (kept !== undefined && kept !== null && kept !== "" && !(Array.isArray(kept) && kept.length === 0))
884
+ write(params.name, null, "code");
885
+ }
886
+ }
887
+ }
595
888
  // Inputs that follow a changed one (`dependsOn`) draw again with their params read afresh, and their rule is asked
596
889
  // again: a getter may have just made one required, read-only or hidden.
597
890
  const followers = this.followersOf(written);
@@ -604,7 +897,7 @@ export class FormStore {
604
897
  if (!shown.has(name))
605
898
  shown.set(name, this.shownWith(name, crossBefore));
606
899
  for (const [name, before] of shown)
607
- if (remounted.has(name) || followers.has(name) || this.shownError(name) !== before)
900
+ if (remounted.has(name) || followers.has(name) || unasked.includes(name) || this.shownError(name) !== before)
608
901
  this.bump(name);
609
902
  this.patchStatus({ changed: this.changedNames.size > 0, message: this.formMessage(), problems: this.problemList() });
610
903
  this.emit(source === "input" ? written[0] : written.length === 1 ? written[0] : null);
@@ -613,15 +906,16 @@ export class FormStore {
613
906
  this.draftDropped = false;
614
907
  this.scheduleDraft();
615
908
  }
616
- /** The inputs whose `dependsOn` names any of `names`. */
909
+ /** The inputs whose `dependsOn` (or `narrowedBy`) names any of `names`. */
617
910
  followersOf(names) {
618
911
  const found = new Set();
619
912
  if (!names.length)
620
913
  return found;
621
914
  const changed = new Set(names);
622
- for (const params of this.options.inputs)
623
- if (params.dependsOn?.some(name => changed.has(name)))
915
+ for (const params of this.options.inputs) {
916
+ if (params.dependsOn?.some(name => changed.has(name)) || (params.narrowedBy?.name !== undefined && changed.has(params.narrowedBy.name)))
624
917
  found.add(params.name);
918
+ }
625
919
  return found;
626
920
  }
627
921
  checkRule(name) {
@@ -631,7 +925,7 @@ export class FormStore {
631
925
  return;
632
926
  }
633
927
  this.staleRules.delete(name);
634
- const message = params && !params.hidden && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
928
+ const message = params && !params.hidden && !isComputedInput(params) && !this.options.locked ? validateInput(params, this.values[name], this.options.context) : null;
635
929
  if (message)
636
930
  this.ruleErrors.set(name, message);
637
931
  else
@@ -657,7 +951,7 @@ export class FormStore {
657
951
  if (!message)
658
952
  continue;
659
953
  const params = this.inputByName.get(name);
660
- if (params && !params.hidden)
954
+ if (params && !params.hidden && !isComputedInput(params))
661
955
  next.set(name, message);
662
956
  else
663
957
  others.push(message);
@@ -751,9 +1045,11 @@ export class FormStore {
751
1045
  }
752
1046
  const entries = [];
753
1047
  const conflicts = [];
1048
+ // An input the draft's own values show (its other input among them) is restored too.
1049
+ const hidden = this.readingFrom({ ...this.values, ...draft.values }, () => new Set(this.options.inputs.filter(params => params.hidden).map(params => params.name)));
754
1050
  for (const params of this.options.inputs) {
755
1051
  const { name } = params;
756
- if (params.hidden || !(name in draft.values) || this.changedNames.has(name))
1052
+ if (hidden.has(name) || isComputedInput(params) || !(name in draft.values) || this.changedNames.has(name))
757
1053
  continue;
758
1054
  const value = draft.values[name];
759
1055
  if (sameData(value, this.values[name]))
@@ -0,0 +1,50 @@
1
+ import { type FormValues, type InputCondition, type InputLabelWhen, type InputParams, type InputSpanWhen } from "../Interfaces/FormInterfaces.ts";
2
+ /** No value: null, undefined, an empty or blank text, an empty list. `false` and `0` are values. */
3
+ export declare function isEmptyValue(value: unknown): boolean;
4
+ /**
5
+ * Whether one condition holds for the other input's value. A list value (a multiple choice) holds `in` when any of its
6
+ * items is listed, and `notIn` when none is. Every key the condition has must hold; one with none holds always.
7
+ */
8
+ export declare function conditionHolds(condition: InputCondition, value: unknown): boolean;
9
+ /** The conditions of a `showWhen` or a `readOnlyWhen`, as a list. */
10
+ export declare function conditionsOf(conditions: InputParams["showWhen"]): readonly InputCondition[];
11
+ /** Whether every condition holds, reading the other inputs' values with `read`. None holds always. */
12
+ export declare function conditionsHold(conditions: InputParams["showWhen"], read: (name: string) => unknown): boolean;
13
+ /** Whether an input with this `showWhen` shows, reading the other inputs' values with `read`: every condition must hold. */
14
+ export declare function inputShown(showWhen: InputParams["showWhen"], read: (name: string) => unknown): boolean;
15
+ /** Whether an input with this `readOnlyWhen` is read-only, reading the other inputs' values with `read`: every condition must hold. */
16
+ export declare function inputReadOnly(readOnlyWhen: InputParams["readOnlyWhen"], read: (name: string) => unknown): boolean;
17
+ /** The text of `texts` for the other input's value (compared as text), or undefined when none is listed for it. */
18
+ export declare function textFor(texts: Readonly<Record<string, string>> | undefined, value: unknown): string | undefined;
19
+ /** The label an input with this `labelWhen` shows for the other input's value: listed, else `otherwise`, else its own. */
20
+ export declare function labelFor(labelWhen: InputLabelWhen, value: unknown, own: InputParams["label"]): InputParams["label"];
21
+ /**
22
+ * The columns an input with this `spanWhen` spans, reading the other inputs' values with `read`: `spanWhen.span` while
23
+ * every condition of `when` holds, else its own `span` (none: undefined, the whole row).
24
+ */
25
+ export declare function spanFor(spanWhen: InputSpanWhen | undefined, read: (name: string) => unknown, own: InputParams["span"]): InputParams["span"];
26
+ /**
27
+ * The inputs with a live `hidden` for each one that has `showWhen`, a live `readOnly` for each one that has
28
+ * `readOnlyWhen` - its own `hidden` / `readOnly` (a value or a getter), or else the conditions, read through `read` as
29
+ * the values are now - a live `label` (and `placeholder`) for each one that has `labelWhen`, and a live `span` for each
30
+ * one that has `spanWhen` (its own `span`, a value or a getter, while the conditions do not hold); and its `dependsOn`
31
+ * joined by the names they read (and by the inputs a `compute` is worked out from), so the form draws it again (and asks
32
+ * its rule again) when they change. The other params are copied with their getters as they are. The same array when no
33
+ * input has any.
34
+ *
35
+ * @example withInputConditions(inputs, name => store.get(name))
36
+ */
37
+ export declare function withInputConditions(inputs: readonly InputParams[], read: (name: string) => unknown): readonly InputParams[];
38
+ /** Whether the input is worked out from others (`compute`): it holds no value of the form's and is never typed in. */
39
+ export declare function isComputedInput(params: InputParams | undefined): boolean;
40
+ /**
41
+ * What a worked-out input (`compute`) shows for these values and the form's `computeWith`. A page's rule that throws
42
+ * shows nothing, and says so in the console, so one bad figure never takes the form down with it.
43
+ */
44
+ export declare function computedValue(params: InputParams, values: FormValues, withValue: unknown): unknown;
45
+ /**
46
+ * A worked-out value as its box writes it: a Number input's figure with its `decimals` always written (1,073.50), grouped,
47
+ * in `locale` (the person's, unless the params name their own); any other input's value as text. Null, undefined, "",
48
+ * NaN, or a figure that is not finite, write nothing, so the box is blank rather than "NaN" or "0.00".
49
+ */
50
+ export declare function computedText(params: InputParams, value: unknown, locale?: string): string;