@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.
- package/README.md +3 -1
- package/dist/accordion.d.ts +0 -1
- package/dist/accordion.js +4 -1
- package/dist/accordion.js.map +1 -1
- package/dist/alert-dialog.d.ts +0 -1
- package/dist/calendar.d.ts +0 -1
- package/dist/calendar.js +4 -1
- package/dist/calendar.js.map +1 -1
- package/dist/carousel.d.ts +0 -1
- package/dist/carousel.js +5 -1
- package/dist/carousel.js.map +1 -1
- package/dist/cascade-list.d.ts +51 -0
- package/dist/cascade-list.js +166 -0
- package/dist/cascade-list.js.map +1 -0
- package/dist/cascade-select.d.ts +1 -2
- package/dist/cascade-select.js +20 -47
- package/dist/cascade-select.js.map +1 -1
- package/dist/checkbox-group.d.ts +0 -1
- package/dist/checkbox.d.ts +0 -1
- package/dist/color-picker.d.ts +0 -1
- package/dist/combobox.d.ts +0 -1
- package/dist/combobox.js +32 -2
- package/dist/combobox.js.map +1 -1
- package/dist/data-grid.d.ts +66 -0
- package/dist/data-grid.js +50 -0
- package/dist/data-grid.js.map +1 -0
- package/dist/data-table.d.ts +100 -0
- package/dist/data-table.js +359 -0
- package/dist/data-table.js.map +1 -0
- package/dist/data-tree-grid.d.ts +73 -0
- package/dist/data-tree-grid.js +53 -0
- package/dist/data-tree-grid.js.map +1 -0
- package/dist/date-field.d.ts +1 -2
- package/dist/date-field.js +17 -5
- package/dist/date-field.js.map +1 -1
- package/dist/date-picker.d.ts +3 -2
- package/dist/date-picker.js +13 -4
- package/dist/date-picker.js.map +1 -1
- package/dist/date-range-field.d.ts +0 -1
- package/dist/date-range-field.js +7 -2
- package/dist/date-range-field.js.map +1 -1
- package/dist/date-range-picker.d.ts +2 -2
- package/dist/date-range-picker.js +10 -5
- package/dist/date-range-picker.js.map +1 -1
- package/dist/date-time-field.d.ts +1 -2
- package/dist/date-time-field.js +15 -4
- package/dist/date-time-field.js.map +1 -1
- package/dist/date-time-picker.d.ts +2 -2
- package/dist/date-time-picker.js +23 -10
- package/dist/date-time-picker.js.map +1 -1
- package/dist/date-time-range-picker.d.ts +2 -2
- package/dist/date-time-range-picker.js +26 -12
- package/dist/date-time-range-picker.js.map +1 -1
- package/dist/dialog.d.ts +0 -1
- package/dist/disclosure.d.ts +0 -1
- package/dist/drawer.d.ts +0 -1
- package/dist/editable.d.ts +0 -1
- package/dist/feed.d.ts +0 -1
- package/dist/feed.js +4 -0
- package/dist/feed.js.map +1 -1
- package/dist/form.d.ts +51 -11
- package/dist/form.js +608 -58
- package/dist/form.js.map +1 -1
- package/dist/grid.d.ts +3 -3
- package/dist/grid.js +8 -4
- package/dist/grid.js.map +1 -1
- package/dist/index.d.ts +8 -30
- package/dist/index.js +4 -17
- package/dist/index.js.map +1 -1
- package/dist/interact-outside.d.ts +0 -1
- package/dist/internal/cascade-choice-binding.d.ts +34 -0
- package/dist/internal/cascade-choice-binding.js +112 -0
- package/dist/internal/cascade-choice-binding.js.map +1 -0
- package/dist/internal/checked-control.d.ts +4 -5
- package/dist/internal/checked-control.js +14 -27
- package/dist/internal/checked-control.js.map +1 -1
- package/dist/internal/controlled-field-input.d.ts +9 -0
- package/dist/internal/controlled-field-input.js +25 -0
- package/dist/internal/controlled-field-input.js.map +1 -0
- package/dist/internal/date-picker-cell.d.ts +0 -1
- package/dist/internal/delegated-event.d.ts +0 -1
- package/dist/internal/direction.d.ts +0 -1
- package/dist/internal/disabled-items.d.ts +0 -1
- package/dist/internal/disabled-items.js +2 -12
- package/dist/internal/disabled-items.js.map +1 -1
- package/dist/internal/exact-percentage.d.ts +2 -0
- package/dist/internal/exact-percentage.js +5 -0
- package/dist/internal/exact-percentage.js.map +1 -0
- package/dist/internal/interaction.d.ts +0 -1
- package/dist/internal/layer-binding.d.ts +0 -1
- package/dist/internal/layer-manager.d.ts +0 -1
- package/dist/internal/menu-control.d.ts +0 -1
- package/dist/internal/menu-control.js +48 -97
- package/dist/internal/menu-control.js.map +1 -1
- package/dist/internal/modal-effects.d.ts +0 -1
- package/dist/internal/picker-position.d.ts +7 -0
- package/dist/internal/picker-position.js +19 -0
- package/dist/internal/picker-position.js.map +1 -0
- package/dist/internal/popup-control.d.ts +2 -3
- package/dist/internal/popup-control.js +8 -10
- package/dist/internal/popup-control.js.map +1 -1
- package/dist/internal/position-connection.d.ts +22 -0
- package/dist/internal/position-connection.js +57 -0
- package/dist/internal/position-connection.js.map +1 -0
- package/dist/internal/positioning/engine.d.ts +65 -0
- package/dist/internal/positioning/engine.js +498 -0
- package/dist/internal/positioning/engine.js.map +1 -0
- package/dist/internal/reference-date.d.ts +0 -1
- package/dist/internal/result.d.ts +0 -3
- package/dist/internal/result.js.map +1 -1
- package/dist/internal/tabular-dom.d.ts +109 -0
- package/dist/internal/tabular-dom.js +316 -0
- package/dist/internal/tabular-dom.js.map +1 -0
- package/dist/internal/tabular-grid-dom.d.ts +151 -0
- package/dist/internal/tabular-grid-dom.js +524 -0
- package/dist/internal/tabular-grid-dom.js.map +1 -0
- package/dist/internal/text-element.d.ts +0 -1
- package/dist/listbox.d.ts +0 -1
- package/dist/listbox.js +5 -0
- package/dist/listbox.js.map +1 -1
- package/dist/menu-button.d.ts +0 -1
- package/dist/menu.d.ts +0 -1
- package/dist/menubar.d.ts +0 -1
- package/dist/meter-group.d.ts +39 -0
- package/dist/meter-group.js +182 -0
- package/dist/meter-group.js.map +1 -0
- package/dist/meter.d.ts +35 -0
- package/dist/meter.js +111 -0
- package/dist/meter.js.map +1 -0
- package/dist/month-picker.d.ts +0 -1
- package/dist/month-range-picker.d.ts +0 -1
- package/dist/multi-thumb-slider.d.ts +0 -1
- package/dist/navigation-menu.d.ts +0 -1
- package/dist/number-field.d.ts +0 -1
- package/dist/number-field.js +8 -1
- package/dist/number-field.js.map +1 -1
- package/dist/pagination.d.ts +0 -1
- package/dist/pin-input.d.ts +0 -1
- package/dist/pin-input.js +5 -3
- package/dist/pin-input.js.map +1 -1
- package/dist/popover.d.ts +6 -22
- package/dist/popover.js +6 -9
- package/dist/popover.js.map +1 -1
- package/dist/position.d.ts +19 -0
- package/dist/position.js +2 -0
- package/dist/position.js.map +1 -0
- package/dist/progress.d.ts +35 -0
- package/dist/progress.js +105 -0
- package/dist/progress.js.map +1 -0
- package/dist/quantity-field.d.ts +0 -1
- package/dist/quantity-field.js +12 -1
- package/dist/quantity-field.js.map +1 -1
- package/dist/radio-group.d.ts +0 -1
- package/dist/radio-group.js +13 -1
- package/dist/radio-group.js.map +1 -1
- package/dist/range-calendar.d.ts +0 -1
- package/dist/rating.d.ts +1 -2
- package/dist/rating.js +191 -27
- package/dist/rating.js.map +1 -1
- package/dist/reorder.d.ts +0 -1
- package/dist/select.d.ts +2 -15
- package/dist/select.js +7 -5
- package/dist/select.js.map +1 -1
- package/dist/slider.d.ts +0 -1
- package/dist/spin-button.d.ts +0 -1
- package/dist/stepper.d.ts +0 -1
- package/dist/switch.d.ts +0 -1
- package/dist/tabs.d.ts +0 -1
- package/dist/tabs.js +11 -2
- package/dist/tabs.js.map +1 -1
- package/dist/tabular.d.ts +3 -0
- package/dist/tabular.js +4 -0
- package/dist/tabular.js.map +1 -0
- package/dist/tags-input.d.ts +0 -1
- package/dist/tags-input.js +8 -4
- package/dist/tags-input.js.map +1 -1
- package/dist/text.d.ts +0 -1
- package/dist/time-field.d.ts +1 -2
- package/dist/time-field.js +11 -5
- package/dist/time-field.js.map +1 -1
- package/dist/time-range-field.d.ts +0 -1
- package/dist/time-range-field.js +3 -1
- package/dist/time-range-field.js.map +1 -1
- package/dist/timer.d.ts +0 -1
- package/dist/toast.d.ts +0 -1
- package/dist/toast.js +32 -5
- package/dist/toast.js.map +1 -1
- package/dist/toggle-button.d.ts +0 -1
- package/dist/toggle-group.d.ts +0 -1
- package/dist/toolbar.d.ts +0 -1
- package/dist/toolbar.js +4 -0
- package/dist/toolbar.js.map +1 -1
- package/dist/tooltip.d.ts +5 -23
- package/dist/tooltip.js +3 -6
- package/dist/tooltip.js.map +1 -1
- package/dist/tree-grid.d.ts +0 -1
- package/dist/tree-grid.js +14 -1
- package/dist/tree-grid.js.map +1 -1
- package/dist/tree-view.d.ts +0 -1
- package/dist/tree-view.js +4 -0
- package/dist/tree-view.js.map +1 -1
- package/dist/virtual.d.ts +2 -3
- package/dist/virtual.js +70 -25
- package/dist/virtual.js.map +1 -1
- package/dist/window-splitter.d.ts +0 -1
- package/dist/year-picker.d.ts +0 -1
- package/dist/year-range-picker.d.ts +0 -1
- package/package.json +135 -85
- package/dist/accordion.d.ts.map +0 -1
- package/dist/alert-dialog.d.ts.map +0 -1
- package/dist/calendar.d.ts.map +0 -1
- package/dist/carousel.d.ts.map +0 -1
- package/dist/cascade-select.d.ts.map +0 -1
- package/dist/checkbox-group.d.ts.map +0 -1
- package/dist/checkbox.d.ts.map +0 -1
- package/dist/color-picker.d.ts.map +0 -1
- package/dist/combobox.d.ts.map +0 -1
- package/dist/date-field.d.ts.map +0 -1
- package/dist/date-picker.d.ts.map +0 -1
- package/dist/date-range-field.d.ts.map +0 -1
- package/dist/date-range-picker.d.ts.map +0 -1
- package/dist/date-time-field.d.ts.map +0 -1
- package/dist/date-time-picker.d.ts.map +0 -1
- package/dist/date-time-range-picker.d.ts.map +0 -1
- package/dist/dialog.d.ts.map +0 -1
- package/dist/disclosure.d.ts.map +0 -1
- package/dist/drawer.d.ts.map +0 -1
- package/dist/editable.d.ts.map +0 -1
- package/dist/feed.d.ts.map +0 -1
- package/dist/form.d.ts.map +0 -1
- package/dist/grid.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interact-outside.d.ts.map +0 -1
- package/dist/internal/checked-control.d.ts.map +0 -1
- package/dist/internal/date-picker-cell.d.ts.map +0 -1
- package/dist/internal/delegated-event.d.ts.map +0 -1
- package/dist/internal/direction.d.ts.map +0 -1
- package/dist/internal/disabled-items.d.ts.map +0 -1
- package/dist/internal/floating-position.d.ts +0 -26
- package/dist/internal/floating-position.d.ts.map +0 -1
- package/dist/internal/floating-position.js +0 -107
- package/dist/internal/floating-position.js.map +0 -1
- package/dist/internal/interaction.d.ts.map +0 -1
- package/dist/internal/layer-binding.d.ts.map +0 -1
- package/dist/internal/layer-manager.d.ts.map +0 -1
- package/dist/internal/menu-control.d.ts.map +0 -1
- package/dist/internal/modal-effects.d.ts.map +0 -1
- package/dist/internal/popup-control.d.ts.map +0 -1
- package/dist/internal/reference-date.d.ts.map +0 -1
- package/dist/internal/result.d.ts.map +0 -1
- package/dist/internal/text-element.d.ts.map +0 -1
- package/dist/listbox.d.ts.map +0 -1
- package/dist/menu-button.d.ts.map +0 -1
- package/dist/menu.d.ts.map +0 -1
- package/dist/menubar.d.ts.map +0 -1
- package/dist/month-picker.d.ts.map +0 -1
- package/dist/month-range-picker.d.ts.map +0 -1
- package/dist/multi-thumb-slider.d.ts.map +0 -1
- package/dist/navigation-menu.d.ts.map +0 -1
- package/dist/number-field.d.ts.map +0 -1
- package/dist/pagination.d.ts.map +0 -1
- package/dist/pin-input.d.ts.map +0 -1
- package/dist/popover.d.ts.map +0 -1
- package/dist/quantity-field.d.ts.map +0 -1
- package/dist/radio-group.d.ts.map +0 -1
- package/dist/range-calendar.d.ts.map +0 -1
- package/dist/rating.d.ts.map +0 -1
- package/dist/reorder.d.ts.map +0 -1
- package/dist/select.d.ts.map +0 -1
- package/dist/slider.d.ts.map +0 -1
- package/dist/spin-button.d.ts.map +0 -1
- package/dist/stepper.d.ts.map +0 -1
- package/dist/switch.d.ts.map +0 -1
- package/dist/tabs.d.ts.map +0 -1
- package/dist/tags-input.d.ts.map +0 -1
- package/dist/text.d.ts.map +0 -1
- package/dist/time-field.d.ts.map +0 -1
- package/dist/time-range-field.d.ts.map +0 -1
- package/dist/timer.d.ts.map +0 -1
- package/dist/toast.d.ts.map +0 -1
- package/dist/toggle-button.d.ts.map +0 -1
- package/dist/toggle-group.d.ts.map +0 -1
- package/dist/toolbar.d.ts.map +0 -1
- package/dist/tooltip.d.ts.map +0 -1
- package/dist/tree-grid.d.ts.map +0 -1
- package/dist/tree-view.d.ts.map +0 -1
- package/dist/virtual.d.ts.map +0 -1
- package/dist/window-splitter.d.ts.map +0 -1
- package/dist/year-picker.d.ts.map +0 -1
- package/dist/year-range-picker.d.ts.map +0 -1
package/dist/form.js
CHANGED
|
@@ -1,31 +1,52 @@
|
|
|
1
|
-
import { applyFormEvent,
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
202
|
+
if (!Object.is(previous, state))
|
|
203
|
+
notify(event, previous);
|
|
87
204
|
return result.value.commands;
|
|
88
205
|
};
|
|
89
|
-
const field = (id) => (state
|
|
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: '
|
|
216
|
+
type: 'set-field-meta',
|
|
96
217
|
id: participant.id,
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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 :
|
|
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
|
|
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
|
-
&&
|
|
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
|
|
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
|
|
319
|
-
|
|
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',
|
|
343
|
-
options.form.addEventListener('change',
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
407
|
-
options.form.removeEventListener('
|
|
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
|
-
|
|
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
|
|
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
|