@sectile/dom 0.7.0 → 0.9.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 (290) hide show
  1. package/README.md +3 -1
  2. package/dist/accordion.d.ts +0 -1
  3. package/dist/accordion.js +4 -1
  4. package/dist/accordion.js.map +1 -1
  5. package/dist/alert-dialog.d.ts +0 -1
  6. package/dist/calendar.d.ts +0 -1
  7. package/dist/calendar.js +4 -1
  8. package/dist/calendar.js.map +1 -1
  9. package/dist/carousel.d.ts +0 -1
  10. package/dist/carousel.js +5 -1
  11. package/dist/carousel.js.map +1 -1
  12. package/dist/cascade-list.d.ts +51 -0
  13. package/dist/cascade-list.js +166 -0
  14. package/dist/cascade-list.js.map +1 -0
  15. package/dist/cascade-select.d.ts +1 -2
  16. package/dist/cascade-select.js +20 -47
  17. package/dist/cascade-select.js.map +1 -1
  18. package/dist/checkbox-group.d.ts +0 -1
  19. package/dist/checkbox.d.ts +0 -1
  20. package/dist/color-picker.d.ts +0 -1
  21. package/dist/combobox.d.ts +0 -1
  22. package/dist/combobox.js +32 -2
  23. package/dist/combobox.js.map +1 -1
  24. package/dist/data-grid.d.ts +66 -0
  25. package/dist/data-grid.js +50 -0
  26. package/dist/data-grid.js.map +1 -0
  27. package/dist/data-table.d.ts +100 -0
  28. package/dist/data-table.js +359 -0
  29. package/dist/data-table.js.map +1 -0
  30. package/dist/data-tree-grid.d.ts +73 -0
  31. package/dist/data-tree-grid.js +53 -0
  32. package/dist/data-tree-grid.js.map +1 -0
  33. package/dist/date-field.d.ts +1 -2
  34. package/dist/date-field.js +17 -5
  35. package/dist/date-field.js.map +1 -1
  36. package/dist/date-picker.d.ts +3 -2
  37. package/dist/date-picker.js +13 -4
  38. package/dist/date-picker.js.map +1 -1
  39. package/dist/date-range-field.d.ts +0 -1
  40. package/dist/date-range-field.js +7 -2
  41. package/dist/date-range-field.js.map +1 -1
  42. package/dist/date-range-picker.d.ts +2 -2
  43. package/dist/date-range-picker.js +10 -5
  44. package/dist/date-range-picker.js.map +1 -1
  45. package/dist/date-time-field.d.ts +1 -2
  46. package/dist/date-time-field.js +15 -4
  47. package/dist/date-time-field.js.map +1 -1
  48. package/dist/date-time-picker.d.ts +2 -2
  49. package/dist/date-time-picker.js +23 -10
  50. package/dist/date-time-picker.js.map +1 -1
  51. package/dist/date-time-range-picker.d.ts +2 -2
  52. package/dist/date-time-range-picker.js +26 -12
  53. package/dist/date-time-range-picker.js.map +1 -1
  54. package/dist/dialog.d.ts +0 -1
  55. package/dist/disclosure.d.ts +0 -1
  56. package/dist/drawer.d.ts +0 -1
  57. package/dist/editable.d.ts +0 -1
  58. package/dist/feed.d.ts +0 -1
  59. package/dist/feed.js +4 -0
  60. package/dist/feed.js.map +1 -1
  61. package/dist/form.d.ts +51 -11
  62. package/dist/form.js +608 -58
  63. package/dist/form.js.map +1 -1
  64. package/dist/grid.d.ts +3 -3
  65. package/dist/grid.js +8 -4
  66. package/dist/grid.js.map +1 -1
  67. package/dist/index.d.ts +8 -30
  68. package/dist/index.js +4 -17
  69. package/dist/index.js.map +1 -1
  70. package/dist/interact-outside.d.ts +0 -1
  71. package/dist/internal/cascade-choice-binding.d.ts +34 -0
  72. package/dist/internal/cascade-choice-binding.js +112 -0
  73. package/dist/internal/cascade-choice-binding.js.map +1 -0
  74. package/dist/internal/checked-control.d.ts +4 -5
  75. package/dist/internal/checked-control.js +14 -27
  76. package/dist/internal/checked-control.js.map +1 -1
  77. package/dist/internal/controlled-field-input.d.ts +9 -0
  78. package/dist/internal/controlled-field-input.js +25 -0
  79. package/dist/internal/controlled-field-input.js.map +1 -0
  80. package/dist/internal/date-picker-cell.d.ts +0 -1
  81. package/dist/internal/delegated-event.d.ts +0 -1
  82. package/dist/internal/direction.d.ts +0 -1
  83. package/dist/internal/disabled-items.d.ts +0 -1
  84. package/dist/internal/disabled-items.js +2 -12
  85. package/dist/internal/disabled-items.js.map +1 -1
  86. package/dist/internal/exact-percentage.d.ts +2 -0
  87. package/dist/internal/exact-percentage.js +5 -0
  88. package/dist/internal/exact-percentage.js.map +1 -0
  89. package/dist/internal/interaction.d.ts +0 -1
  90. package/dist/internal/layer-binding.d.ts +0 -1
  91. package/dist/internal/layer-manager.d.ts +0 -1
  92. package/dist/internal/menu-control.d.ts +0 -1
  93. package/dist/internal/menu-control.js +48 -97
  94. package/dist/internal/menu-control.js.map +1 -1
  95. package/dist/internal/modal-effects.d.ts +0 -1
  96. package/dist/internal/picker-position.d.ts +7 -0
  97. package/dist/internal/picker-position.js +19 -0
  98. package/dist/internal/picker-position.js.map +1 -0
  99. package/dist/internal/popup-control.d.ts +2 -3
  100. package/dist/internal/popup-control.js +8 -10
  101. package/dist/internal/popup-control.js.map +1 -1
  102. package/dist/internal/position-connection.d.ts +22 -0
  103. package/dist/internal/position-connection.js +57 -0
  104. package/dist/internal/position-connection.js.map +1 -0
  105. package/dist/internal/positioning/engine.d.ts +65 -0
  106. package/dist/internal/positioning/engine.js +498 -0
  107. package/dist/internal/positioning/engine.js.map +1 -0
  108. package/dist/internal/reference-date.d.ts +0 -1
  109. package/dist/internal/result.d.ts +0 -3
  110. package/dist/internal/result.js.map +1 -1
  111. package/dist/internal/tabular-dom.d.ts +109 -0
  112. package/dist/internal/tabular-dom.js +316 -0
  113. package/dist/internal/tabular-dom.js.map +1 -0
  114. package/dist/internal/tabular-grid-dom.d.ts +151 -0
  115. package/dist/internal/tabular-grid-dom.js +524 -0
  116. package/dist/internal/tabular-grid-dom.js.map +1 -0
  117. package/dist/internal/text-element.d.ts +0 -1
  118. package/dist/listbox.d.ts +0 -1
  119. package/dist/listbox.js +5 -0
  120. package/dist/listbox.js.map +1 -1
  121. package/dist/menu-button.d.ts +0 -1
  122. package/dist/menu.d.ts +0 -1
  123. package/dist/menubar.d.ts +0 -1
  124. package/dist/meter-group.d.ts +39 -0
  125. package/dist/meter-group.js +182 -0
  126. package/dist/meter-group.js.map +1 -0
  127. package/dist/meter.d.ts +35 -0
  128. package/dist/meter.js +111 -0
  129. package/dist/meter.js.map +1 -0
  130. package/dist/month-picker.d.ts +0 -1
  131. package/dist/month-range-picker.d.ts +0 -1
  132. package/dist/multi-thumb-slider.d.ts +0 -1
  133. package/dist/navigation-menu.d.ts +0 -1
  134. package/dist/number-field.d.ts +0 -1
  135. package/dist/number-field.js +8 -1
  136. package/dist/number-field.js.map +1 -1
  137. package/dist/pagination.d.ts +0 -1
  138. package/dist/pin-input.d.ts +0 -1
  139. package/dist/pin-input.js +5 -3
  140. package/dist/pin-input.js.map +1 -1
  141. package/dist/popover.d.ts +6 -22
  142. package/dist/popover.js +6 -9
  143. package/dist/popover.js.map +1 -1
  144. package/dist/position.d.ts +19 -0
  145. package/dist/position.js +2 -0
  146. package/dist/position.js.map +1 -0
  147. package/dist/progress.d.ts +35 -0
  148. package/dist/progress.js +105 -0
  149. package/dist/progress.js.map +1 -0
  150. package/dist/quantity-field.d.ts +0 -1
  151. package/dist/quantity-field.js +12 -1
  152. package/dist/quantity-field.js.map +1 -1
  153. package/dist/radio-group.d.ts +0 -1
  154. package/dist/radio-group.js +13 -1
  155. package/dist/radio-group.js.map +1 -1
  156. package/dist/range-calendar.d.ts +0 -1
  157. package/dist/rating.d.ts +1 -2
  158. package/dist/rating.js +191 -27
  159. package/dist/rating.js.map +1 -1
  160. package/dist/reorder.d.ts +0 -1
  161. package/dist/select.d.ts +2 -15
  162. package/dist/select.js +7 -5
  163. package/dist/select.js.map +1 -1
  164. package/dist/slider.d.ts +0 -1
  165. package/dist/spin-button.d.ts +0 -1
  166. package/dist/stepper.d.ts +0 -1
  167. package/dist/switch.d.ts +0 -1
  168. package/dist/tabs.d.ts +0 -1
  169. package/dist/tabs.js +11 -2
  170. package/dist/tabs.js.map +1 -1
  171. package/dist/tabular.d.ts +3 -0
  172. package/dist/tabular.js +4 -0
  173. package/dist/tabular.js.map +1 -0
  174. package/dist/tags-input.d.ts +0 -1
  175. package/dist/tags-input.js +8 -4
  176. package/dist/tags-input.js.map +1 -1
  177. package/dist/text.d.ts +0 -1
  178. package/dist/time-field.d.ts +1 -2
  179. package/dist/time-field.js +11 -5
  180. package/dist/time-field.js.map +1 -1
  181. package/dist/time-range-field.d.ts +0 -1
  182. package/dist/time-range-field.js +3 -1
  183. package/dist/time-range-field.js.map +1 -1
  184. package/dist/timer.d.ts +0 -1
  185. package/dist/toast.d.ts +0 -1
  186. package/dist/toast.js +32 -5
  187. package/dist/toast.js.map +1 -1
  188. package/dist/toggle-button.d.ts +0 -1
  189. package/dist/toggle-group.d.ts +0 -1
  190. package/dist/toolbar.d.ts +0 -1
  191. package/dist/toolbar.js +4 -0
  192. package/dist/toolbar.js.map +1 -1
  193. package/dist/tooltip.d.ts +5 -23
  194. package/dist/tooltip.js +3 -6
  195. package/dist/tooltip.js.map +1 -1
  196. package/dist/tree-grid.d.ts +0 -1
  197. package/dist/tree-grid.js +14 -1
  198. package/dist/tree-grid.js.map +1 -1
  199. package/dist/tree-view.d.ts +0 -1
  200. package/dist/tree-view.js +4 -0
  201. package/dist/tree-view.js.map +1 -1
  202. package/dist/virtual.d.ts +2 -3
  203. package/dist/virtual.js +70 -25
  204. package/dist/virtual.js.map +1 -1
  205. package/dist/window-splitter.d.ts +0 -1
  206. package/dist/year-picker.d.ts +0 -1
  207. package/dist/year-range-picker.d.ts +0 -1
  208. package/package.json +135 -85
  209. package/dist/accordion.d.ts.map +0 -1
  210. package/dist/alert-dialog.d.ts.map +0 -1
  211. package/dist/calendar.d.ts.map +0 -1
  212. package/dist/carousel.d.ts.map +0 -1
  213. package/dist/cascade-select.d.ts.map +0 -1
  214. package/dist/checkbox-group.d.ts.map +0 -1
  215. package/dist/checkbox.d.ts.map +0 -1
  216. package/dist/color-picker.d.ts.map +0 -1
  217. package/dist/combobox.d.ts.map +0 -1
  218. package/dist/date-field.d.ts.map +0 -1
  219. package/dist/date-picker.d.ts.map +0 -1
  220. package/dist/date-range-field.d.ts.map +0 -1
  221. package/dist/date-range-picker.d.ts.map +0 -1
  222. package/dist/date-time-field.d.ts.map +0 -1
  223. package/dist/date-time-picker.d.ts.map +0 -1
  224. package/dist/date-time-range-picker.d.ts.map +0 -1
  225. package/dist/dialog.d.ts.map +0 -1
  226. package/dist/disclosure.d.ts.map +0 -1
  227. package/dist/drawer.d.ts.map +0 -1
  228. package/dist/editable.d.ts.map +0 -1
  229. package/dist/feed.d.ts.map +0 -1
  230. package/dist/form.d.ts.map +0 -1
  231. package/dist/grid.d.ts.map +0 -1
  232. package/dist/index.d.ts.map +0 -1
  233. package/dist/interact-outside.d.ts.map +0 -1
  234. package/dist/internal/checked-control.d.ts.map +0 -1
  235. package/dist/internal/date-picker-cell.d.ts.map +0 -1
  236. package/dist/internal/delegated-event.d.ts.map +0 -1
  237. package/dist/internal/direction.d.ts.map +0 -1
  238. package/dist/internal/disabled-items.d.ts.map +0 -1
  239. package/dist/internal/floating-position.d.ts +0 -26
  240. package/dist/internal/floating-position.d.ts.map +0 -1
  241. package/dist/internal/floating-position.js +0 -107
  242. package/dist/internal/floating-position.js.map +0 -1
  243. package/dist/internal/interaction.d.ts.map +0 -1
  244. package/dist/internal/layer-binding.d.ts.map +0 -1
  245. package/dist/internal/layer-manager.d.ts.map +0 -1
  246. package/dist/internal/menu-control.d.ts.map +0 -1
  247. package/dist/internal/modal-effects.d.ts.map +0 -1
  248. package/dist/internal/popup-control.d.ts.map +0 -1
  249. package/dist/internal/reference-date.d.ts.map +0 -1
  250. package/dist/internal/result.d.ts.map +0 -1
  251. package/dist/internal/text-element.d.ts.map +0 -1
  252. package/dist/listbox.d.ts.map +0 -1
  253. package/dist/menu-button.d.ts.map +0 -1
  254. package/dist/menu.d.ts.map +0 -1
  255. package/dist/menubar.d.ts.map +0 -1
  256. package/dist/month-picker.d.ts.map +0 -1
  257. package/dist/month-range-picker.d.ts.map +0 -1
  258. package/dist/multi-thumb-slider.d.ts.map +0 -1
  259. package/dist/navigation-menu.d.ts.map +0 -1
  260. package/dist/number-field.d.ts.map +0 -1
  261. package/dist/pagination.d.ts.map +0 -1
  262. package/dist/pin-input.d.ts.map +0 -1
  263. package/dist/popover.d.ts.map +0 -1
  264. package/dist/quantity-field.d.ts.map +0 -1
  265. package/dist/radio-group.d.ts.map +0 -1
  266. package/dist/range-calendar.d.ts.map +0 -1
  267. package/dist/rating.d.ts.map +0 -1
  268. package/dist/reorder.d.ts.map +0 -1
  269. package/dist/select.d.ts.map +0 -1
  270. package/dist/slider.d.ts.map +0 -1
  271. package/dist/spin-button.d.ts.map +0 -1
  272. package/dist/stepper.d.ts.map +0 -1
  273. package/dist/switch.d.ts.map +0 -1
  274. package/dist/tabs.d.ts.map +0 -1
  275. package/dist/tags-input.d.ts.map +0 -1
  276. package/dist/text.d.ts.map +0 -1
  277. package/dist/time-field.d.ts.map +0 -1
  278. package/dist/time-range-field.d.ts.map +0 -1
  279. package/dist/timer.d.ts.map +0 -1
  280. package/dist/toast.d.ts.map +0 -1
  281. package/dist/toggle-button.d.ts.map +0 -1
  282. package/dist/toggle-group.d.ts.map +0 -1
  283. package/dist/toolbar.d.ts.map +0 -1
  284. package/dist/tooltip.d.ts.map +0 -1
  285. package/dist/tree-grid.d.ts.map +0 -1
  286. package/dist/tree-view.d.ts.map +0 -1
  287. package/dist/virtual.d.ts.map +0 -1
  288. package/dist/window-splitter.d.ts.map +0 -1
  289. package/dist/year-picker.d.ts.map +0 -1
  290. package/dist/year-range-picker.d.ts.map +0 -1
package/dist/form.js CHANGED
@@ -1,31 +1,52 @@
1
- import { applyFormEvent, createFormValues, encodeFormFieldPath, tryCreateFormState, } from '@sectile/core/form';
2
- import { unwrap } from '@sectile/core/result';
3
- export { appendFormFieldPath, createFormFieldPath, createFormRelativePath, encodeFormFieldPath, } from '@sectile/core/form';
1
+ import { applyFormEvent, getFormField, getFormFieldIDsByIssueSource, tryCreateFormState, } from '@sectile/form/state';
2
+ import { encodeFormFieldPath, tryCreateFormFieldPath, } from '@sectile/form/path';
3
+ import { tryCreateFormValues } from '@sectile/form/values';
4
+ import { FormResultError } from '@sectile/form/error';
5
+ export { appendFormFieldPath, createFormFieldPath, createFormRelativePath, encodeFormFieldPath, } from '@sectile/form/path';
6
+ export function defineFormSubmission(definition) {
7
+ return Object.freeze({ ...definition });
8
+ }
4
9
  export function createForm(options) {
5
- return unwrap(tryCreateForm(options));
10
+ const result = tryCreateForm(options);
11
+ if (!result.ok)
12
+ throw new FormResultError(result.error);
13
+ return result.value;
6
14
  }
7
15
  export function tryCreateForm(options) {
8
16
  const initial = tryCreateFormState({ issues: options.issues ?? [] });
9
17
  if (!initial.ok)
10
18
  return initial;
11
19
  const participants = new Map();
12
- const subscribers = new Set();
20
+ const participantBaselines = new Map();
21
+ const participantValues = new Map();
22
+ const participantObservers = new Map();
23
+ const handledParticipantEvents = new WeakSet();
24
+ const pendingReinitializations = new Map();
25
+ const formSubscribers = new Set();
26
+ const fieldSubscribers = new Map();
27
+ const pendingNotifications = [];
13
28
  let state = initial.value;
14
29
  let revision = 0;
15
30
  let active = true;
31
+ let recoveringFocus = false;
16
32
  let invalidBatchPending = false;
17
33
  let validationSequence = 0;
18
34
  let validationController = null;
35
+ let nativeResume = null;
36
+ let nativeResumeToken = 0;
19
37
  let summary = options.summary;
20
38
  let schemaOption = options.schema;
21
39
  let validateOption = options.validate;
22
40
  let validateOn = new Set(options.validateOn ?? []);
23
41
  let revalidateOn = new Set(options.revalidateOn ?? ['input']);
24
42
  let submitHandler = options.onSubmit;
43
+ let submitErrorMapper = options.mapSubmitError;
25
44
  let resetHandler = options.onReset;
26
45
  let announceSummaryHandler = options.onAnnounceSummary;
27
46
  let stateChangeHandler = options.onStateChange;
28
47
  let updateHandler = options.onUpdate;
48
+ let subscriptionErrorHandler = options.onSubscriptionError;
49
+ let dispatchingNotifications = false;
29
50
  options.form.dataset['scope'] = 'form';
30
51
  options.form.dataset['part'] = 'root';
31
52
  const configureSummary = (element) => {
@@ -56,10 +77,12 @@ export function tryCreateForm(options) {
56
77
  validateOn = new Set(nextValidateOn);
57
78
  revalidateOn = new Set(nextRevalidateOn);
58
79
  submitHandler = next.onSubmit;
80
+ submitErrorMapper = next.mapSubmitError;
59
81
  resetHandler = next.onReset;
60
82
  announceSummaryHandler = next.onAnnounceSummary;
61
83
  stateChangeHandler = next.onStateChange;
62
84
  updateHandler = next.onUpdate;
85
+ subscriptionErrorHandler = next.onSubscriptionError;
63
86
  if (!validationChanged)
64
87
  return;
65
88
  validationController?.abort();
@@ -70,34 +93,165 @@ export function tryCreateForm(options) {
70
93
  transition({ type: 'replace-issues', source: 'schema', issues: [] });
71
94
  };
72
95
  const snapshot = () => Object.freeze({ revision, state });
73
- const notify = () => {
96
+ const reportSubscriptionError = (error) => {
97
+ try {
98
+ subscriptionErrorHandler?.(error);
99
+ }
100
+ catch { /* subscriber failures stay isolated */ }
101
+ };
102
+ const dispatchSubscriptions = (subscriptions, value, notificationRevision) => {
103
+ if (subscriptions === undefined || subscriptions.size === 0)
104
+ return;
105
+ for (const subscription of [...subscriptions]) {
106
+ if (!subscription.active || subscription.createdRevision >= notificationRevision)
107
+ continue;
108
+ let selected;
109
+ try {
110
+ selected = subscription.select(value);
111
+ }
112
+ catch (error) {
113
+ reportSubscriptionError(error);
114
+ continue;
115
+ }
116
+ let equal;
117
+ try {
118
+ equal = subscription.equals(subscription.selected, selected);
119
+ }
120
+ catch (error) {
121
+ reportSubscriptionError(error);
122
+ continue;
123
+ }
124
+ if (equal)
125
+ continue;
126
+ const previous = subscription.selected;
127
+ subscription.selected = selected;
128
+ try {
129
+ subscription.listener(selected, previous);
130
+ }
131
+ catch (error) {
132
+ reportSubscriptionError(error);
133
+ }
134
+ }
135
+ };
136
+ const affectedFieldIDs = (event, previous, next) => {
137
+ if (event === 'reset' || (typeof event !== 'string' && event.type === 'reinitialize')) {
138
+ return null;
139
+ }
140
+ if (typeof event === 'string')
141
+ return [];
142
+ if (event.type === 'register-field'
143
+ || event.type === 'unregister-field'
144
+ || event.type === 'set-field-meta'
145
+ || event.type === 'replace-field-issues'
146
+ || event.type === 'upsert-field-issue'
147
+ || event.type === 'remove-field-issue'
148
+ || event.type === 'clear-field-issues')
149
+ return [event.type === 'register-field' ? event.field.id : event.id];
150
+ const source = event.type === 'replace-issues'
151
+ ? event.source
152
+ : event.type === 'submit-failed' || event.type === 'submit-succeeded'
153
+ || (event.type === 'validation-started' && event.intent === 'submission')
154
+ ? 'server'
155
+ : null;
156
+ if (source === null)
157
+ return [];
158
+ return [...new Set([
159
+ ...getFormFieldIDsByIssueSource(previous, source),
160
+ ...getFormFieldIDsByIssueSource(next, source),
161
+ ])];
162
+ };
163
+ const notify = (event, previous) => {
74
164
  revision += 1;
75
- const next = snapshot();
76
- stateChangeHandler?.(state);
77
- updateHandler?.();
78
- for (const subscriber of subscribers)
79
- subscriber(next);
165
+ pendingNotifications.push({ event, previous, next: state, revision });
166
+ if (dispatchingNotifications)
167
+ return;
168
+ dispatchingNotifications = true;
169
+ try {
170
+ for (let index = 0; index < pendingNotifications.length; index += 1) {
171
+ const notification = pendingNotifications[index];
172
+ stateChangeHandler?.(notification.next);
173
+ updateHandler?.();
174
+ dispatchSubscriptions(formSubscribers, notification.next, notification.revision);
175
+ if (!active)
176
+ break;
177
+ const affected = affectedFieldIDs(notification.event, notification.previous, notification.next);
178
+ if (affected === null) {
179
+ for (const [id, subscriptions] of fieldSubscribers) {
180
+ dispatchSubscriptions(subscriptions, getFormField(notification.next, id), notification.revision);
181
+ }
182
+ continue;
183
+ }
184
+ for (const id of affected) {
185
+ dispatchSubscriptions(fieldSubscribers.get(id), getFormField(notification.next, id), notification.revision);
186
+ }
187
+ }
188
+ }
189
+ finally {
190
+ pendingNotifications.length = 0;
191
+ dispatchingNotifications = false;
192
+ }
80
193
  };
81
194
  const transition = (event) => {
195
+ if (!active)
196
+ return null;
82
197
  const result = applyFormEvent(state, event);
83
198
  if (!result.ok)
84
199
  return null;
200
+ const previous = state;
85
201
  state = result.value.state;
86
- notify();
202
+ if (!Object.is(previous, state))
203
+ notify(event, previous);
87
204
  return result.value.commands;
88
205
  };
89
- const field = (id) => (state.fields.find((candidate) => candidate.id === id));
206
+ const field = (id) => (getFormField(state, id) ?? undefined);
90
207
  const updateParticipant = (participant, flags = {}) => {
91
208
  const current = field(participant.id);
92
209
  if (current === undefined)
93
210
  return false;
211
+ const touched = flags.touched ?? current.touched;
212
+ const dirty = flags.dirty ?? current.dirty;
213
+ if (touched === current.touched && dirty === current.dirty)
214
+ return true;
94
215
  return transition({
95
- type: 'update-field',
216
+ type: 'set-field-meta',
96
217
  id: participant.id,
97
- touched: flags.touched ?? current.touched,
98
- dirty: flags.dirty ?? current.dirty,
218
+ meta: { touched, dirty },
99
219
  }) !== null;
100
220
  };
221
+ const valuesEqual = (participant, current, baseline) => participant.isValueEqual?.(current, baseline)
222
+ ?? sameParticipantValue(current, baseline);
223
+ const readValue = (participant) => (participant.getValue === undefined
224
+ ? readParticipantValue(participant)
225
+ : participant.getValue());
226
+ const captureParticipant = (participant) => {
227
+ const value = readValue(participant);
228
+ participantValues.set(participant.id, value);
229
+ return value;
230
+ };
231
+ const captureAllCurrentValues = () => {
232
+ if (!active)
233
+ return;
234
+ for (const participant of participants.values()) {
235
+ const value = captureParticipant(participant);
236
+ participantBaselines.set(participant.id, value);
237
+ }
238
+ };
239
+ const reinitialize = (reinitializeOptions = {}) => {
240
+ if (!active)
241
+ return;
242
+ validationController?.abort();
243
+ validationController = null;
244
+ validationSequence += 1;
245
+ nativeResume = null;
246
+ pendingReinitializations.clear();
247
+ captureAllCurrentValues();
248
+ transition({ type: 'reinitialize', options: reinitializeOptions });
249
+ if (summary !== undefined) {
250
+ const remaining = orderedIssues(state);
251
+ summary.textContent = remaining.map((issue) => issue.message).join(' ');
252
+ summary.hidden = remaining.length === 0;
253
+ }
254
+ };
101
255
  const focusInvalid = (startId) => {
102
256
  const invalid = state.fields.filter((candidate) => !candidate.valid || candidate.issues.length > 0);
103
257
  const start = Math.max(0, invalid.findIndex((candidate) => candidate.id === startId));
@@ -108,7 +262,7 @@ export function tryCreateForm(options) {
108
262
  continue;
109
263
  if (participant.focus !== undefined) {
110
264
  if (participant.focus() !== false)
111
- return;
265
+ return true;
112
266
  continue;
113
267
  }
114
268
  const control = participant.focusTarget
@@ -118,8 +272,10 @@ export function tryCreateForm(options) {
118
272
  if (focusable.hidden || focusable.disabled === true)
119
273
  continue;
120
274
  focusable.focus();
121
- return;
275
+ if (focusable.ownerDocument.activeElement === focusable)
276
+ return true;
122
277
  }
278
+ return false;
123
279
  };
124
280
  const announce = (issueIds) => {
125
281
  const issues = orderedIssues(state).filter((issue) => issueIds.includes(issue.id));
@@ -130,11 +286,30 @@ export function tryCreateForm(options) {
130
286
  announceSummaryHandler?.(issues);
131
287
  };
132
288
  const execute = (commands) => {
289
+ let focused = false;
133
290
  for (const command of commands) {
134
- if (command.type === 'focus-field')
135
- focusInvalid(command.id);
136
- if (command.type === 'announce-summary')
291
+ if (command.type === 'focus-field') {
292
+ recoveringFocus = true;
293
+ try {
294
+ focused = focusInvalid(command.id) || focused;
295
+ }
296
+ finally {
297
+ recoveringFocus = false;
298
+ }
299
+ }
300
+ if (command.type === 'announce-summary') {
137
301
  announce(command.issueIds);
302
+ if (!focused && command.issueIds.length > 0 && summary !== undefined) {
303
+ recoveringFocus = true;
304
+ try {
305
+ summary.focus();
306
+ focused = summary.ownerDocument.activeElement === summary;
307
+ }
308
+ finally {
309
+ recoveringFocus = false;
310
+ }
311
+ }
312
+ }
138
313
  if (command.type === 'reset-field')
139
314
  participants.get(command.id)?.reset?.();
140
315
  }
@@ -159,10 +334,8 @@ export function tryCreateForm(options) {
159
334
  return Object.freeze(issues);
160
335
  };
161
336
  const mapValidationIssues = (source, issues) => Object.freeze(issues.map((issue, index) => {
162
- const name = issue.path === undefined ? null : encodeFormFieldPath(issue.path);
163
- const owner = name === null
164
- ? undefined
165
- : state.fields.find((candidate) => candidate.name === name);
337
+ const name = issue.path === undefined ? null : safeEncodeFormFieldPath(issue.path);
338
+ const owner = name === null ? undefined : findIssueOwner(state, name);
166
339
  return Object.freeze({
167
340
  id: `${source}:${name ?? 'form'}:${index}`,
168
341
  message: issue.message,
@@ -170,7 +343,7 @@ export function tryCreateForm(options) {
170
343
  ...(owner === undefined ? {} : { fieldId: owner.id }),
171
344
  });
172
345
  }));
173
- const finishValidation = (sequence, generation, trigger, intent, formData, input, submitter, event, custom, schema, includeNative) => {
346
+ const finishValidation = (sequence, generation, trigger, intent, formData, input, submitter, event, custom, schema, includeNative, resumeNative) => {
174
347
  if (!active || sequence !== validationSequence)
175
348
  return;
176
349
  if (intent === 'submission') {
@@ -213,14 +386,91 @@ export function tryCreateForm(options) {
213
386
  }
214
387
  if (event === null)
215
388
  return;
216
- submitHandler?.({
389
+ if (submitHandler === undefined) {
390
+ if (resumeNative)
391
+ resumeNativeSubmission(submitter);
392
+ return;
393
+ }
394
+ event.preventDefault();
395
+ const submissionGeneration = state.submissionGeneration;
396
+ if (transition({ type: 'submit-started', generation: submissionGeneration }) === null)
397
+ return;
398
+ const payload = Object.freeze({
217
399
  event,
218
400
  formData,
219
401
  values: schema !== null && schema.issues === undefined ? schema.value : input,
220
402
  submitter,
221
403
  state,
404
+ reinitialize: (reinitializeOptions = {}) => {
405
+ if (!active || submissionGeneration !== state.submissionGeneration)
406
+ return;
407
+ pendingReinitializations.set(submissionGeneration, reinitializeOptions);
408
+ },
222
409
  });
410
+ let result;
411
+ try {
412
+ result = submitHandler(payload);
413
+ }
414
+ catch (error) {
415
+ failManagedSubmission(submissionGeneration, error);
416
+ return;
417
+ }
418
+ if (isPromiseLike(result)) {
419
+ void Promise.resolve(result).then((resolved) => settleManagedSubmission(submissionGeneration, resolved), (error) => failManagedSubmission(submissionGeneration, error));
420
+ return;
421
+ }
422
+ settleManagedSubmission(submissionGeneration, result);
223
423
  };
424
+ const settleManagedSubmission = (generation, result) => {
425
+ if (!active || generation !== state.submissionGeneration)
426
+ return;
427
+ if (typeof result === 'object' && result !== null && result.ok === false) {
428
+ pendingReinitializations.delete(generation);
429
+ let commands = transition({
430
+ type: 'submit-failed',
431
+ generation,
432
+ issues: normalizeSubmissionIssues(result.issues ?? [defaultSubmissionIssue(generation)]),
433
+ });
434
+ if (commands === null && state.submissionStatus === 'submitting') {
435
+ commands = transition({
436
+ type: 'submit-failed',
437
+ generation,
438
+ issues: [defaultSubmissionIssue(generation)],
439
+ });
440
+ }
441
+ if (commands !== null)
442
+ execute(commands);
443
+ return;
444
+ }
445
+ const succeeded = transition({ type: 'submit-succeeded', generation });
446
+ if (succeeded === null)
447
+ return;
448
+ const requested = pendingReinitializations.get(generation);
449
+ pendingReinitializations.delete(generation);
450
+ if (requested !== undefined)
451
+ reinitialize(requested);
452
+ };
453
+ const failManagedSubmission = (generation, reason) => {
454
+ if (!active || generation !== state.submissionGeneration)
455
+ return;
456
+ let issues;
457
+ try {
458
+ issues = submitErrorMapper?.(reason) ?? [defaultSubmissionIssue(generation)];
459
+ }
460
+ catch {
461
+ issues = [defaultSubmissionIssue(generation)];
462
+ }
463
+ settleManagedSubmission(generation, { ok: false, issues });
464
+ };
465
+ const normalizeSubmissionIssues = (issues) => Object.freeze(issues.map((issue) => Object.freeze({
466
+ ...issue,
467
+ source: 'server',
468
+ })));
469
+ const defaultSubmissionIssue = (generation) => Object.freeze({
470
+ id: `server:submission:${generation}`,
471
+ message: 'Form submission failed.',
472
+ source: 'server',
473
+ });
224
474
  const runValidation = (trigger, intent, changedFieldId, event = null, submitter = null) => {
225
475
  if (state.submissionStatus === 'submitting') {
226
476
  event?.preventDefault();
@@ -236,7 +486,36 @@ export function tryCreateForm(options) {
236
486
  execute(commands);
237
487
  const generation = state.validationGeneration;
238
488
  const formData = createNativeFormData(options.form, submitter);
239
- const input = createFormValues([...formData.entries()].map(([path, value]) => ({ path, value })));
489
+ const values = tryCreateFormValues([...formData.entries()].map(([path, value]) => ({ path, value })));
490
+ if (!values.ok) {
491
+ if (intent === 'submission') {
492
+ transition({
493
+ type: 'replace-issues',
494
+ source: 'native',
495
+ issues: includeNativeValidation(options.form, submitter) ? collectNativeIssues() : [],
496
+ generation,
497
+ });
498
+ }
499
+ transition({
500
+ type: 'replace-issues',
501
+ source: 'validate',
502
+ issues: [Object.freeze({
503
+ id: `validate:form-values:${generation}`,
504
+ message: 'Form values could not be constructed safely.',
505
+ source: 'validate',
506
+ })],
507
+ generation,
508
+ });
509
+ if (intent === 'submission') {
510
+ transition({ type: 'replace-issues', source: 'schema', issues: [], generation });
511
+ }
512
+ const completed = transition({ type: 'validation-completed', trigger, intent, generation });
513
+ if (completed !== null)
514
+ execute(completed);
515
+ event?.preventDefault();
516
+ return;
517
+ }
518
+ const input = values.value;
240
519
  const context = Object.freeze({
241
520
  trigger,
242
521
  intent,
@@ -260,8 +539,7 @@ export function tryCreateForm(options) {
260
539
  }
261
540
  }
262
541
  const includeNative = intent === 'submission'
263
- && !options.form.noValidate
264
- && !isFormNoValidateSubmitter(submitter);
542
+ && includeNativeValidation(options.form, submitter);
265
543
  if (isPromiseLike(custom) || isPromiseLike(schema)) {
266
544
  event?.preventDefault();
267
545
  void Promise.all([
@@ -272,16 +550,49 @@ export function tryCreateForm(options) {
272
550
  ]).then(([customResult, schemaResult]) => {
273
551
  if (controller.signal.aborted)
274
552
  return;
275
- finishValidation(sequence, generation, trigger, intent, formData, input, submitter, event, customResult, schemaResult, includeNative);
553
+ finishValidation(sequence, generation, trigger, intent, formData, input, submitter, event, customResult, schemaResult, includeNative, event !== null && submitHandler === undefined);
276
554
  });
277
555
  return;
278
556
  }
279
- finishValidation(sequence, generation, trigger, intent, formData, input, submitter, event, custom, schema, includeNative);
557
+ finishValidation(sequence, generation, trigger, intent, formData, input, submitter, event, custom, schema, includeNative, false);
558
+ };
559
+ const resumeNativeSubmission = (submitter) => {
560
+ if (!active)
561
+ return;
562
+ const token = ++nativeResumeToken;
563
+ nativeResume = { token, submitter };
564
+ try {
565
+ const associatedForm = submitter === null
566
+ ? null
567
+ : submitter.form;
568
+ if (submitter !== null
569
+ && isNativeSubmitter(submitter)
570
+ && associatedForm === options.form) {
571
+ options.form.requestSubmit(submitter);
572
+ }
573
+ else {
574
+ options.form.requestSubmit();
575
+ }
576
+ }
577
+ finally {
578
+ queueMicrotask(() => {
579
+ if (nativeResume?.token === token)
580
+ nativeResume = null;
581
+ });
582
+ }
280
583
  };
281
584
  const submit = (event) => {
282
585
  const submitter = event.submitter instanceof HTMLElement && isNativeSubmitter(event.submitter)
283
586
  ? event.submitter
284
587
  : null;
588
+ if (nativeResume !== null && nativeResume.submitter === submitter) {
589
+ nativeResume = null;
590
+ return;
591
+ }
592
+ if (state.validationStatus === 'validating' && state.validationIntent === 'submission') {
593
+ event.preventDefault();
594
+ return;
595
+ }
285
596
  runValidation('submit', 'submission', null, event, submitter);
286
597
  };
287
598
  const submitInvalidBatch = () => {
@@ -300,14 +611,10 @@ export function tryCreateForm(options) {
300
611
  || participant.element.contains(target);
301
612
  });
302
613
  };
303
- const interact = (event, trigger) => {
304
- const participant = participantFor(event.target);
614
+ const invalidateAfterInteraction = (trigger, participant) => {
305
615
  const previousIntent = state.validationStatus === 'invalid'
306
616
  ? state.validationIntent
307
617
  : null;
308
- if (participant !== undefined) {
309
- updateParticipant(participant, trigger === 'input' ? { dirty: true } : { touched: true });
310
- }
311
618
  if (previousIntent !== null && !revalidateOn.has(trigger))
312
619
  return;
313
620
  transition({ type: 'validation-invalidated' });
@@ -315,9 +622,45 @@ export function tryCreateForm(options) {
315
622
  if (intent !== null)
316
623
  runValidation(trigger, intent, participant?.id ?? null);
317
624
  };
318
- const onInput = (event) => interact(event, 'input');
319
- const onBlur = (event) => interact(event, 'blur');
625
+ const onValueInteraction = (event) => {
626
+ if (handledParticipantEvents.has(event))
627
+ return;
628
+ handledParticipantEvents.add(event);
629
+ const participant = participantFor(event.target);
630
+ if (participant === undefined)
631
+ return;
632
+ const updateValue = () => {
633
+ if (!active || participants.get(participant.id) !== participant)
634
+ return;
635
+ const previous = participantValues.get(participant.id);
636
+ const current = readValue(participant);
637
+ if (valuesEqual(participant, current, previous))
638
+ return;
639
+ participantValues.set(participant.id, current);
640
+ const baseline = participantBaselines.get(participant.id);
641
+ updateParticipant(participant, {
642
+ dirty: !valuesEqual(participant, current, baseline),
643
+ });
644
+ invalidateAfterInteraction('input', participant);
645
+ };
646
+ if (event.type === 'input' || event.type === 'change')
647
+ updateValue();
648
+ else
649
+ queueMicrotask(updateValue);
650
+ };
651
+ const onBlur = (event) => {
652
+ if (recoveringFocus || handledParticipantEvents.has(event))
653
+ return;
654
+ handledParticipantEvents.add(event);
655
+ const participant = participantFor(event.target);
656
+ if (participant !== undefined)
657
+ updateParticipant(participant, { touched: true });
658
+ invalidateAfterInteraction('blur', participant);
659
+ };
320
660
  const onInvalid = (event) => {
661
+ if (handledParticipantEvents.has(event))
662
+ return;
663
+ handledParticipantEvents.add(event);
321
664
  const participant = participantFor(event.target);
322
665
  if (participant !== undefined)
323
666
  updateParticipant(participant, { touched: true });
@@ -330,6 +673,8 @@ export function tryCreateForm(options) {
330
673
  const onReset = () => {
331
674
  validationController?.abort();
332
675
  validationSequence += 1;
676
+ nativeResume = null;
677
+ pendingReinitializations.clear();
333
678
  const commands = transition('reset');
334
679
  if (commands !== null)
335
680
  execute(commands);
@@ -338,34 +683,113 @@ export function tryCreateForm(options) {
338
683
  summary.hidden = true;
339
684
  }
340
685
  resetHandler?.();
686
+ queueMicrotask(captureAllCurrentValues);
341
687
  };
342
- options.form.addEventListener('input', onInput, true);
343
- options.form.addEventListener('change', onInput, true);
688
+ options.form.addEventListener('input', onValueInteraction);
689
+ options.form.addEventListener('change', onValueInteraction);
344
690
  options.form.addEventListener('blur', onBlur, true);
345
691
  options.form.addEventListener('invalid', onInvalid, true);
346
692
  options.form.addEventListener('submit', onSubmit);
347
693
  options.form.addEventListener('reset', onReset);
694
+ const observeParticipant = (participant) => {
695
+ participantObservers.get(participant.id)?.();
696
+ const externalTargets = participantTargets(participant).filter((target) => !options.form.contains(target));
697
+ const roots = externalTargets.filter((target) => !externalTargets.some((candidate) => candidate !== target && candidate.contains(target)));
698
+ for (const target of roots) {
699
+ target.addEventListener('input', onValueInteraction);
700
+ target.addEventListener('change', onValueInteraction);
701
+ target.addEventListener('blur', onBlur, true);
702
+ target.addEventListener('invalid', onInvalid, true);
703
+ }
704
+ participantObservers.set(participant.id, () => {
705
+ for (const target of roots) {
706
+ target.removeEventListener('input', onValueInteraction);
707
+ target.removeEventListener('change', onValueInteraction);
708
+ target.removeEventListener('blur', onBlur, true);
709
+ target.removeEventListener('invalid', onInvalid, true);
710
+ }
711
+ });
712
+ };
348
713
  const registerParticipant = (participant) => {
714
+ const replacing = participants.has(participant.id);
715
+ participantObservers.get(participant.id)?.();
349
716
  participants.set(participant.id, participant);
717
+ const currentValue = captureParticipant(participant);
718
+ if (!replacing || !participantBaselines.has(participant.id)) {
719
+ participantBaselines.set(participant.id, currentValue);
720
+ }
721
+ observeParticipant(participant);
350
722
  participant.element.dataset['scope'] = 'form';
351
723
  participant.element.dataset['part'] = 'field';
352
- transition({
353
- type: 'register-field',
354
- field: {
724
+ if (replacing) {
725
+ transition({
726
+ type: 'set-field-meta',
355
727
  id: participant.id,
356
- name: readParticipantName(participant),
357
- },
358
- });
728
+ meta: { name: readParticipantName(participant) },
729
+ });
730
+ }
731
+ else {
732
+ transition({
733
+ type: 'register-field',
734
+ field: {
735
+ id: participant.id,
736
+ name: readParticipantName(participant),
737
+ },
738
+ });
739
+ }
359
740
  reorderParticipants(participants, state, transition);
360
741
  return () => {
361
742
  if (participants.get(participant.id) !== participant)
362
743
  return;
744
+ participantObservers.get(participant.id)?.();
745
+ participantObservers.delete(participant.id);
363
746
  participants.delete(participant.id);
747
+ participantBaselines.delete(participant.id);
748
+ participantValues.delete(participant.id);
364
749
  transition({ type: 'unregister-field', id: participant.id });
365
750
  };
366
751
  };
367
752
  for (const participant of options.participants ?? [])
368
753
  registerParticipant(participant);
754
+ const createSelectorSubscription = (value, selector, listener, subscribeOptions = {}) => ({
755
+ select: selector,
756
+ listener: listener,
757
+ equals: (subscribeOptions.equals ?? Object.is),
758
+ selected: selector(value),
759
+ active: true,
760
+ createdRevision: revision,
761
+ });
762
+ const subscribeForm = (selector, listener, subscribeOptions) => {
763
+ if (!active)
764
+ return () => { };
765
+ const subscription = createSelectorSubscription(state, selector, listener, subscribeOptions);
766
+ formSubscribers.add(subscription);
767
+ return () => {
768
+ if (!subscription.active)
769
+ return;
770
+ subscription.active = false;
771
+ formSubscribers.delete(subscription);
772
+ };
773
+ };
774
+ const subscribeField = (id, selector, listener, subscribeOptions) => {
775
+ if (!active)
776
+ return () => { };
777
+ const subscription = createSelectorSubscription(getFormField(state, id), selector, listener, subscribeOptions);
778
+ let channel = fieldSubscribers.get(id);
779
+ if (channel === undefined) {
780
+ channel = new Set();
781
+ fieldSubscribers.set(id, channel);
782
+ }
783
+ channel.add(subscription);
784
+ return () => {
785
+ if (!subscription.active)
786
+ return;
787
+ subscription.active = false;
788
+ channel?.delete(subscription);
789
+ if (channel?.size === 0)
790
+ fieldSubscribers.delete(id);
791
+ };
792
+ };
369
793
  const connection = {
370
794
  get state() { return state; },
371
795
  getSnapshot: snapshot,
@@ -376,10 +800,45 @@ export function tryCreateForm(options) {
376
800
  const participant = participants.get(id);
377
801
  if (participant === undefined)
378
802
  return false;
803
+ observeParticipant(participant);
804
+ const currentField = field(id);
805
+ const name = readParticipantName(participant);
806
+ if (currentField?.name !== name) {
807
+ transition({ type: 'set-field-meta', id, meta: { name } });
808
+ }
809
+ const previous = participantValues.get(id);
810
+ const current = captureParticipant(participant);
811
+ const baseline = participantBaselines.get(id);
812
+ const valueChanged = !valuesEqual(participant, current, previous);
813
+ if (valueChanged) {
814
+ updateParticipant(participant, { dirty: !valuesEqual(participant, current, baseline) });
815
+ }
816
+ reorderParticipants(participants, state, transition);
817
+ if (!valueChanged)
818
+ return true;
819
+ const previousIntent = state.validationStatus === 'invalid'
820
+ ? state.validationIntent
821
+ : null;
379
822
  transition({ type: 'validation-invalidated' });
380
- runValidation('input', 'interaction', id);
823
+ if (previousIntent !== null)
824
+ runValidation('input', previousIntent, id);
381
825
  return true;
382
826
  },
827
+ getField: (id) => getFormField(state, id),
828
+ setFieldMeta: (id, meta) => transition({ type: 'set-field-meta', id, meta }) !== null,
829
+ replaceFieldIssues: (id, source, issues) => transition({
830
+ type: 'replace-field-issues',
831
+ id,
832
+ source,
833
+ issues,
834
+ }) !== null,
835
+ upsertFieldIssue: (id, issue) => transition({ type: 'upsert-field-issue', id, issue }) !== null,
836
+ removeFieldIssue: (id, issueId) => transition({ type: 'remove-field-issue', id, issueId }) !== null,
837
+ clearFieldIssues: (id, source) => transition({
838
+ type: 'clear-field-issues',
839
+ id,
840
+ ...(source === undefined ? {} : { source }),
841
+ }) !== null,
383
842
  replaceIssues: (source, issues) => transition({ type: 'replace-issues', source, issues }) !== null,
384
843
  submitStarted: () => {
385
844
  const generation = state.submissionGeneration;
@@ -393,35 +852,100 @@ export function tryCreateForm(options) {
393
852
  execute(commands);
394
853
  return true;
395
854
  },
855
+ reinitialize,
396
856
  reset: () => options.form.reset(),
397
- subscribe: (listener) => {
398
- subscribers.add(listener);
399
- return () => { subscribers.delete(listener); };
400
- },
857
+ subscribeForm,
858
+ subscribeField,
401
859
  destroy: () => {
402
860
  if (!active)
403
861
  return;
404
862
  active = false;
863
+ pendingNotifications.length = 0;
405
864
  validationController?.abort();
406
- options.form.removeEventListener('input', onInput, true);
407
- options.form.removeEventListener('change', onInput, true);
865
+ nativeResume = null;
866
+ options.form.removeEventListener('input', onValueInteraction);
867
+ options.form.removeEventListener('change', onValueInteraction);
408
868
  options.form.removeEventListener('blur', onBlur, true);
409
869
  options.form.removeEventListener('invalid', onInvalid, true);
410
870
  options.form.removeEventListener('submit', onSubmit);
411
871
  options.form.removeEventListener('reset', onReset);
412
- subscribers.clear();
872
+ for (const subscription of formSubscribers)
873
+ subscription.active = false;
874
+ formSubscribers.clear();
875
+ for (const channel of fieldSubscribers.values()) {
876
+ for (const subscription of channel)
877
+ subscription.active = false;
878
+ }
879
+ fieldSubscribers.clear();
880
+ for (const disconnect of participantObservers.values())
881
+ disconnect();
882
+ participantObservers.clear();
413
883
  participants.clear();
884
+ participantBaselines.clear();
885
+ participantValues.clear();
886
+ pendingReinitializations.clear();
414
887
  },
415
888
  };
416
889
  return { ok: true, value: connection };
417
890
  }
891
+ function readParticipantValue(participant) {
892
+ const explicit = participant.submissionElements ?? [];
893
+ const candidates = explicit.length > 0
894
+ ? explicit
895
+ : [participant.semanticControl ?? participant.element].filter(isSubmissionElement);
896
+ return Object.freeze(candidates.map(readSubmissionValue));
897
+ }
898
+ function isSubmissionElement(element) {
899
+ return element.tagName === 'BUTTON'
900
+ || element.tagName === 'INPUT'
901
+ || element.tagName === 'SELECT'
902
+ || element.tagName === 'TEXTAREA';
903
+ }
904
+ function readSubmissionValue(element) {
905
+ if (element.tagName === 'SELECT') {
906
+ const select = element;
907
+ return Object.freeze([
908
+ 'select',
909
+ select.multiple,
910
+ Object.freeze([...select.options]
911
+ .filter((option) => option.selected)
912
+ .map((option) => option.value)),
913
+ ]);
914
+ }
915
+ if (element.tagName === 'INPUT') {
916
+ const input = element;
917
+ const type = input.type.toLowerCase();
918
+ if (type === 'checkbox' || type === 'radio') {
919
+ return Object.freeze(['checked', input.checked, input.value]);
920
+ }
921
+ if (type === 'file') {
922
+ return Object.freeze([
923
+ 'files',
924
+ Object.freeze(Array.from(input.files ?? []).map((file) => Object.freeze([
925
+ file.name,
926
+ file.size,
927
+ file.type,
928
+ file.lastModified,
929
+ ]))),
930
+ ]);
931
+ }
932
+ }
933
+ return Object.freeze(['value', element.value]);
934
+ }
935
+ function sameParticipantValue(left, right) {
936
+ if (Object.is(left, right))
937
+ return true;
938
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
939
+ return false;
940
+ return left.every((value, index) => sameParticipantValue(value, right[index]));
941
+ }
418
942
  function readControlName(element) {
419
943
  const name = element.name?.trim();
420
944
  return name === undefined || name.length === 0 ? null : name;
421
945
  }
422
946
  function readParticipantName(participant) {
423
947
  if (participant.name !== undefined && participant.name !== null) {
424
- return encodeFormFieldPath(participant.name);
948
+ return safeEncodeFormFieldPath(participant.name);
425
949
  }
426
950
  for (const element of participant.submissionElements ?? []) {
427
951
  const name = readControlName(element);
@@ -435,13 +959,15 @@ function participantTargets(participant) {
435
959
  participant.element,
436
960
  participant.semanticControl,
437
961
  participant.focusTarget,
962
+ participant.validationTarget,
438
963
  ...(participant.submissionElements ?? []),
439
964
  ].filter((element) => element !== undefined))];
440
965
  }
441
966
  function validationTargets(participant) {
442
967
  const semantic = participant.semanticControl;
968
+ const validation = participant.validationTarget;
443
969
  const submissions = participant.submissionElements ?? [];
444
- const targets = [...new Set([semantic, ...submissions].filter((element) => element !== undefined))];
970
+ const targets = [...new Set([validation, semantic, ...submissions].filter((element) => element !== undefined))];
445
971
  return targets.length > 0 ? targets : [participant.element];
446
972
  }
447
973
  function createNativeFormData(form, submitter) {
@@ -462,6 +988,29 @@ function isFormNoValidateSubmitter(element) {
462
988
  return false;
463
989
  return element.formNoValidate === true;
464
990
  }
991
+ function includeNativeValidation(form, submitter) {
992
+ return !form.noValidate && !isFormNoValidateSubmitter(submitter);
993
+ }
994
+ function safeEncodeFormFieldPath(path) {
995
+ const result = tryCreateFormFieldPath(path);
996
+ return result.ok ? encodeFormFieldPath(result.value) : null;
997
+ }
998
+ function findIssueOwner(state, issueName) {
999
+ let owner;
1000
+ for (const candidate of state.fields) {
1001
+ if (candidate.name === null || !ownsIssuePath(candidate.name, issueName))
1002
+ continue;
1003
+ if (owner === undefined || candidate.name.length > (owner.name?.length ?? 0)) {
1004
+ owner = candidate;
1005
+ }
1006
+ }
1007
+ return owner;
1008
+ }
1009
+ function ownsIssuePath(fieldName, issueName) {
1010
+ return issueName === fieldName
1011
+ || issueName.startsWith(`${fieldName}.`)
1012
+ || issueName.startsWith(`${fieldName}[`);
1013
+ }
465
1014
  function isPromiseLike(value) {
466
1015
  return value !== null
467
1016
  && typeof value === 'object'
@@ -504,7 +1053,8 @@ function reorderParticipants(participants, state, transition) {
504
1053
  return byRegistration.indexOf(left) - byRegistration.indexOf(right);
505
1054
  });
506
1055
  const ids = ordered.map((participant) => participant.id);
507
- if (ids.length === state.fields.length)
1056
+ if (ids.length === state.fields.length
1057
+ && ids.some((id, index) => state.fields[index]?.id !== id))
508
1058
  transition({ type: 'reorder-fields', ids });
509
1059
  }
510
1060
  //# sourceMappingURL=form.js.map