@verbb/formie-browser 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/chunks/{address-finder-CQW81kY0.js → address-finder-BjMuEiwF.js} +2 -2
- package/dist/chunks/{bpoint-D8OjLmsn.js → bpoint-KudsBCo2.js} +1 -1
- package/dist/chunks/{calculations-BqZsENCq.js → calculations-DlGSkQ3W.js} +2 -2
- package/dist/chunks/{captcha-eu-DIa6aUWz.js → captcha-eu-CterYFok.js} +2 -2
- package/dist/chunks/{checkbox-radio-BI-UjgMF.js → checkbox-radio-CBvgk3hR.js} +2 -2
- package/dist/chunks/{conditions-BVl6jTYo.js → conditions-DYy1-Hwt.js} +189 -163
- package/dist/chunks/{date-picker-69TgvErA.js → date-picker-BlD--zfe.js} +2 -2
- package/dist/chunks/{eway-1GPxH6DU.js → eway-qwc-B5Us.js} +2 -2
- package/dist/chunks/file-upload-CIgHrXfp.js +316 -0
- package/dist/chunks/{friendly-captcha-v1-DA2QPKA3.js → friendly-captcha-v1-EDFac33c.js} +1 -1
- package/dist/chunks/{friendly-captcha-v2-BorOWiz_.js → friendly-captcha-v2-BDpSvFGR.js} +1 -1
- package/dist/chunks/{go-cardless-DwBgERcA.js → go-cardless-DuDMVsnz.js} +1 -1
- package/dist/chunks/{google-address-BcTBncyS.js → google-address-Bmjp01pH.js} +77 -58
- package/dist/chunks/{hcaptcha-BchdVCqq.js → hcaptcha-BTQ6An6g.js} +2 -2
- package/dist/chunks/{hidden-D8RqSPt7.js → hidden-DbPdOxKb.js} +1 -1
- package/dist/chunks/{index-C1ZOKiAi.js → index-BcqwpPNE.js} +1261 -1176
- package/dist/chunks/{loqate-nXuTX7In.js → loqate-BdNGv93o.js} +2 -2
- package/dist/chunks/{mollie-qdCynQCd.js → mollie-DFOjTuGX.js} +1 -1
- package/dist/chunks/{moneris-BNEA-laO.js → moneris-BbFfl-EL.js} +1 -1
- package/dist/chunks/{opayo-BJtXOkuK.js → opayo-CmgYa9ya.js} +2 -2
- package/dist/chunks/{paddle-D72Pa8kr.js → paddle-BUi7w1_o.js} +2 -2
- package/dist/chunks/{paypal-Cqt2bned.js → paypal-A-ePfh6k.js} +2 -2
- package/dist/chunks/{payway-BjCqsc0H.js → payway-BqFCbhyo.js} +2 -2
- package/dist/chunks/{phone-country-K9pYAlzM.js → phone-country-DPMiqk4L.js} +2 -2
- package/dist/chunks/{place-kit-Dg6f_UoM.js → place-kit-CX7tamAg.js} +1 -1
- package/dist/chunks/{recaptcha-enterprise--VYAeeqz.js → recaptcha-enterprise-DxF6cW95.js} +2 -2
- package/dist/chunks/{recaptcha-shared-PSIEEEe1.js → recaptcha-shared-DhGcbiaq.js} +2 -2
- package/dist/chunks/{recaptcha-v2-checkbox-atBNe5ml.js → recaptcha-v2-checkbox-R9EfbZ4k.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-DnBufNPT.js → recaptcha-v2-invisible-DV9hswAp.js} +2 -2
- package/dist/chunks/{recaptcha-v3-CF6O8rDj.js → recaptcha-v3-V0xc-Edh.js} +2 -2
- package/dist/chunks/{repeater-CB7Yojvx.js → repeater-DKUT6TGI.js} +2 -2
- package/dist/chunks/{rich-text-nAV_SQZo.js → rich-text-SH7KiaKH.js} +1 -1
- package/dist/chunks/{scripts-BkAAux0i.js → scripts-D9N2d5rN.js} +1 -1
- package/dist/chunks/{shared-DGqeMvcJ.js → shared-BmntPmnW.js} +4 -4
- package/dist/chunks/{signature-BT6CsQ6n.js → signature-BA3tQsGT.js} +1 -1
- package/dist/chunks/{snaptcha-C7qQo02f.js → snaptcha-DV7p6J9k.js} +1 -1
- package/dist/chunks/{square-C5AH3trb.js → square-Bs-ndqYK.js} +2 -2
- package/dist/chunks/{stripe-EJK0RyUv.js → stripe-DkZAHxtn.js} +2 -2
- package/dist/chunks/{summary-DC_ipXCU.js → summary-Dq2i9TQk.js} +2 -2
- package/dist/chunks/{table-BEAI99qo.js → table-Cu4WfVcw.js} +2 -2
- package/dist/chunks/{text-limit-Blgq5G-G.js → text-limit-2ycmB-Ae.js} +1 -1
- package/dist/chunks/{turnstile-_uCRwnU-.js → turnstile-CId90tRr.js} +2 -2
- package/dist/css/theme/forms/_form.css +28 -0
- package/dist/index.js +1 -1
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/modules/address/google-address.d.ts +7 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions/effects.d.ts +3 -1
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/submit/pipeline.d.ts.map +1 -1
- package/dist/js/validation/scope.d.ts +6 -0
- package/dist/js/validation/scope.d.ts.map +1 -0
- package/dist/js/validation/submit-readiness.d.ts +4 -0
- package/dist/js/validation/submit-readiness.d.ts.map +1 -0
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/css/theme/forms/_form.css +28 -0
- package/src/js/core/create-formie-client.ts +6 -0
- package/src/js/core/submit-result-state.ts +17 -0
- package/src/js/modules/address/google-address.ts +33 -1
- package/src/js/modules/fields/conditions/effects.ts +79 -2
- package/src/js/modules/fields/conditions.ts +53 -11
- package/src/js/modules/fields/file-upload.ts +188 -96
- package/src/js/submit/pipeline.ts +3 -28
- package/src/js/validation/scope.ts +25 -0
- package/src/js/validation/submit-readiness.ts +157 -0
- package/src/js/validation/validator.ts +4 -0
- package/dist/chunks/file-upload-BDZZftgL.js +0 -281
|
@@ -8,6 +8,7 @@ type GoogleMaps = typeof google;
|
|
|
8
8
|
type GoogleAddressProviderOptions = {
|
|
9
9
|
apiKey?: string;
|
|
10
10
|
options?: Record<string, unknown>;
|
|
11
|
+
countryDefaultValue?: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
const SCRIPT_ID = 'FORMIE_GOOGLE_ADDRESS_SCRIPT';
|
|
@@ -194,6 +195,37 @@ function buildAddressFromComponents(formData: Record<string, string>): {
|
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
function hasCountryRestriction(options: Record<string, unknown>): boolean {
|
|
199
|
+
const componentRestrictions = options.componentRestrictions;
|
|
200
|
+
|
|
201
|
+
if (componentRestrictions && typeof componentRestrictions === 'object') {
|
|
202
|
+
const country = (componentRestrictions as { country?: unknown }).country;
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(country) ? country.length > 0 : Boolean(country)) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const includedRegionCodes = options.includedRegionCodes;
|
|
210
|
+
|
|
211
|
+
return Array.isArray(includedRegionCodes) && includedRegionCodes.length > 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function buildGoogleAutocompleteOptions(provider: GoogleAddressProviderOptions): Record<string, unknown> {
|
|
215
|
+
const options = { types: ['geocode'], ...(provider.options || {}) };
|
|
216
|
+
const country = provider.countryDefaultValue?.trim().toLowerCase();
|
|
217
|
+
|
|
218
|
+
if (!country || hasCountryRestriction(options)) {
|
|
219
|
+
return options;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...options,
|
|
224
|
+
componentRestrictions: { country },
|
|
225
|
+
includedRegionCodes: [country.toUpperCase()],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
export const googleAddressModule = defineAddressModule<
|
|
198
230
|
GoogleAddressProviderOptions,
|
|
199
231
|
GoogleMaps,
|
|
@@ -218,7 +250,7 @@ export const googleAddressModule = defineAddressModule<
|
|
|
218
250
|
return null;
|
|
219
251
|
}
|
|
220
252
|
|
|
221
|
-
const options =
|
|
253
|
+
const options = buildGoogleAutocompleteOptions(provider);
|
|
222
254
|
const autocomplete = new PlaceAutocompleteElement(options);
|
|
223
255
|
const inputHeight = window.getComputedStyle(input).height;
|
|
224
256
|
|
|
@@ -3,6 +3,9 @@ const PRESERVED_DISABLED_ATTR = 'data-formie-preserve-disabled';
|
|
|
3
3
|
const CONDITIONAL_HIDDEN_ATTR = 'data-formie-conditionally-hidden';
|
|
4
4
|
const PAGE_HIDDEN_ATTR = 'data-formie-page-hidden';
|
|
5
5
|
const CONDITIONAL_HIDDEN_CLASS = 'formie-conditionally-hidden';
|
|
6
|
+
const CP_MUTED_CLASS = 'fui-cp-muted-conditional-field';
|
|
7
|
+
const CP_MUTED_EXPANDED_CLASS = 'fui-cp-muted-conditional-field--expanded';
|
|
8
|
+
const CP_MUTED_ATTR = 'data-formie-cp-muted';
|
|
6
9
|
const PAGE_HIDDEN_CLASS = 'formie-page-hidden';
|
|
7
10
|
const ROW_HIDDEN_ATTR = 'data-formie-row-hidden';
|
|
8
11
|
const ROW_HIDDEN_CLASS = 'formie-row-hidden';
|
|
@@ -143,6 +146,19 @@ function syncRowState(row: Element): void {
|
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
function clearCpMutedState(node: Element): void {
|
|
150
|
+
node.removeAttribute(CP_MUTED_ATTR);
|
|
151
|
+
node.classList.remove(CP_MUTED_CLASS);
|
|
152
|
+
node.classList.remove(CP_MUTED_EXPANDED_CLASS);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clearHideState(node: Element): void {
|
|
156
|
+
node.removeAttribute(CONDITIONAL_HIDDEN_ATTR);
|
|
157
|
+
node.removeAttribute(PAGE_HIDDEN_ATTR);
|
|
158
|
+
node.classList.remove(CONDITIONAL_HIDDEN_CLASS);
|
|
159
|
+
node.classList.remove(PAGE_HIDDEN_CLASS);
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
function syncAncestorRows(node: Element): void {
|
|
147
163
|
let currentRow = node.closest(ROW_SELECTOR);
|
|
148
164
|
|
|
@@ -152,8 +168,26 @@ function syncAncestorRows(node: Element): void {
|
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
170
|
|
|
155
|
-
export function applyConditionVisibility(
|
|
156
|
-
|
|
171
|
+
export function applyConditionVisibility(
|
|
172
|
+
node: Element,
|
|
173
|
+
hidden: boolean,
|
|
174
|
+
clearOnHide: boolean,
|
|
175
|
+
options: { displayMode?: 'hide' | 'muted' } = {},
|
|
176
|
+
): boolean {
|
|
177
|
+
if (options.displayMode === 'muted') {
|
|
178
|
+
return applyMutedConditionVisibility(node, hidden);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let stateChanged = false;
|
|
182
|
+
|
|
183
|
+
if (node.hasAttribute(CP_MUTED_ATTR)
|
|
184
|
+
|| node.classList.contains(CP_MUTED_CLASS)
|
|
185
|
+
|| node.classList.contains(CP_MUTED_EXPANDED_CLASS)) {
|
|
186
|
+
clearCpMutedState(node);
|
|
187
|
+
stateChanged = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
stateChanged = setVisibilityState(node, hidden) || stateChanged;
|
|
157
191
|
|
|
158
192
|
syncDisabledState(node, hidden);
|
|
159
193
|
syncAncestorRows(node);
|
|
@@ -164,3 +198,46 @@ export function applyConditionVisibility(node: Element, hidden: boolean, clearOn
|
|
|
164
198
|
|
|
165
199
|
return stateChanged;
|
|
166
200
|
}
|
|
201
|
+
|
|
202
|
+
function applyMutedConditionVisibility(node: Element, hidden: boolean): boolean {
|
|
203
|
+
let stateChanged = false;
|
|
204
|
+
|
|
205
|
+
if (hidden) {
|
|
206
|
+
if (node.hasAttribute(CONDITIONAL_HIDDEN_ATTR)
|
|
207
|
+
|| node.hasAttribute(PAGE_HIDDEN_ATTR)
|
|
208
|
+
|| node.classList.contains(CONDITIONAL_HIDDEN_CLASS)
|
|
209
|
+
|| node.classList.contains(PAGE_HIDDEN_CLASS)) {
|
|
210
|
+
clearHideState(node);
|
|
211
|
+
stateChanged = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!node.hasAttribute(CP_MUTED_ATTR)) {
|
|
215
|
+
node.setAttribute(CP_MUTED_ATTR, 'true');
|
|
216
|
+
stateChanged = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!node.classList.contains(CP_MUTED_CLASS)) {
|
|
220
|
+
node.classList.add(CP_MUTED_CLASS);
|
|
221
|
+
stateChanged = true;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
if (node.hasAttribute(CP_MUTED_ATTR)
|
|
225
|
+
|| node.classList.contains(CP_MUTED_CLASS)
|
|
226
|
+
|| node.classList.contains(CP_MUTED_EXPANDED_CLASS)) {
|
|
227
|
+
clearCpMutedState(node);
|
|
228
|
+
stateChanged = true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (node.hasAttribute(CONDITIONAL_HIDDEN_ATTR)
|
|
232
|
+
|| node.hasAttribute(PAGE_HIDDEN_ATTR)
|
|
233
|
+
|| node.classList.contains(CONDITIONAL_HIDDEN_CLASS)
|
|
234
|
+
|| node.classList.contains(PAGE_HIDDEN_CLASS)) {
|
|
235
|
+
clearHideState(node);
|
|
236
|
+
stateChanged = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
syncAncestorRows(node);
|
|
241
|
+
|
|
242
|
+
return stateChanged;
|
|
243
|
+
}
|
|
@@ -72,6 +72,9 @@ export const conditionsModule: FormieModuleDefinition = {
|
|
|
72
72
|
});
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const cpDisplayMode = ctx.options?.cpDisplayMode === 'muted' ? 'muted' : 'hide';
|
|
76
|
+
let isApplyingConditions = false;
|
|
77
|
+
|
|
75
78
|
const runEvaluationPass = (): boolean => {
|
|
76
79
|
let hasStateChanges = false;
|
|
77
80
|
|
|
@@ -80,7 +83,16 @@ export const conditionsModule: FormieModuleDefinition = {
|
|
|
80
83
|
return queryConditionInputs(scopeRoot, entry.node, condition);
|
|
81
84
|
});
|
|
82
85
|
|
|
83
|
-
const
|
|
86
|
+
const displayMode = entry.node.hasAttribute('data-formie-page')
|
|
87
|
+
? 'hide'
|
|
88
|
+
: cpDisplayMode;
|
|
89
|
+
|
|
90
|
+
const stateChanged = applyConditionVisibility(
|
|
91
|
+
entry.node,
|
|
92
|
+
result.shouldHide,
|
|
93
|
+
entry.settings.clearOnHide,
|
|
94
|
+
{ displayMode },
|
|
95
|
+
);
|
|
84
96
|
hasStateChanges = hasStateChanges || stateChanged;
|
|
85
97
|
debug.log('Condition evaluated.', {
|
|
86
98
|
shouldHide: result.shouldHide,
|
|
@@ -100,26 +112,45 @@ export const conditionsModule: FormieModuleDefinition = {
|
|
|
100
112
|
};
|
|
101
113
|
|
|
102
114
|
const evaluateAll = (): void => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
if (isApplyingConditions) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isApplyingConditions = true;
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
try {
|
|
122
|
+
for (let pass = 0; pass < MAX_EVALUATION_PASSES; pass += 1) {
|
|
123
|
+
if (!runEvaluationPass()) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (pass === MAX_EVALUATION_PASSES - 1) {
|
|
128
|
+
debug.warn('Reached max evaluation passes.', { maxPasses: MAX_EVALUATION_PASSES });
|
|
129
|
+
}
|
|
110
130
|
}
|
|
131
|
+
} finally {
|
|
132
|
+
isApplyingConditions = false;
|
|
111
133
|
}
|
|
112
134
|
};
|
|
113
135
|
|
|
114
136
|
const scheduleEvaluateAll = (): void => {
|
|
137
|
+
if (isApplyingConditions) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
if (evaluationQueued) {
|
|
116
142
|
return;
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
evaluationQueued = true;
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
requestAnimationFrame(() => {
|
|
122
148
|
evaluationQueued = false;
|
|
149
|
+
|
|
150
|
+
if (isApplyingConditions) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
123
154
|
evaluateAll();
|
|
124
155
|
});
|
|
125
156
|
};
|
|
@@ -168,19 +199,32 @@ export const conditionsModule: FormieModuleDefinition = {
|
|
|
168
199
|
};
|
|
169
200
|
|
|
170
201
|
const scheduleRebuild = (): void => {
|
|
202
|
+
if (isApplyingConditions) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
if (rebuildQueued) {
|
|
172
207
|
return;
|
|
173
208
|
}
|
|
174
209
|
|
|
175
210
|
rebuildQueued = true;
|
|
176
211
|
|
|
177
|
-
|
|
212
|
+
requestAnimationFrame(() => {
|
|
178
213
|
rebuildQueued = false;
|
|
214
|
+
|
|
215
|
+
if (isApplyingConditions) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
179
219
|
rebuild();
|
|
180
220
|
});
|
|
181
221
|
};
|
|
182
222
|
|
|
183
223
|
const observer = new MutationObserver((mutations) => {
|
|
224
|
+
if (isApplyingConditions) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
const shouldRebuild = mutations.some((mutation) => {
|
|
185
229
|
return mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0);
|
|
186
230
|
});
|
|
@@ -200,8 +244,6 @@ export const conditionsModule: FormieModuleDefinition = {
|
|
|
200
244
|
subtree: true,
|
|
201
245
|
attributes: true,
|
|
202
246
|
attributeFilter: [
|
|
203
|
-
'class',
|
|
204
|
-
'style',
|
|
205
247
|
'hidden',
|
|
206
248
|
'aria-hidden',
|
|
207
249
|
'data-formie-conditionally-hidden',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fileUploadCss from '#theme-css/fields/_file.css?inline';
|
|
2
2
|
|
|
3
3
|
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
4
|
-
import { dispatchFieldEvent,
|
|
4
|
+
import { dispatchFieldEvent, releaseFormValidators, retainFormValidators } from '#modules/fields/shared';
|
|
5
5
|
import { ensureModuleStyles } from '#modules/styles';
|
|
6
|
-
import { getFileUploadEventName, getFormStateEventName } from '#utils/event-names';
|
|
6
|
+
import { getFieldModuleEventName, getFileUploadEventName, getFormStateEventName } from '#utils/event-names';
|
|
7
7
|
import { requestJson } from '#utils/http';
|
|
8
8
|
|
|
9
9
|
const INPUT_SELECTOR = 'input[type="file"][data-formie-file-input]';
|
|
@@ -20,6 +20,8 @@ const FILE_VALIDATORS = [
|
|
|
20
20
|
] as const;
|
|
21
21
|
const VALIDATOR_SCOPE = 'file-upload';
|
|
22
22
|
const MODULE_ID = 'file-upload';
|
|
23
|
+
const REPEATER_INIT_ROW_EVENT = getFieldModuleEventName('repeater', 'init-row');
|
|
24
|
+
const FIELD_HANDLE_SELECTOR = '[data-formie-field-handle]';
|
|
23
25
|
|
|
24
26
|
ensureModuleStyles(MODULE_ID, [fileUploadCss]);
|
|
25
27
|
|
|
@@ -366,6 +368,47 @@ function updateUploadedAssetInputs(field: HTMLElement, input: HTMLInputElement,
|
|
|
366
368
|
input.value = '';
|
|
367
369
|
}
|
|
368
370
|
|
|
371
|
+
function getFormHandle(form: HTMLFormElement | null): string {
|
|
372
|
+
if (!form) {
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const handleInput = form.querySelector('input[name="handle"]');
|
|
377
|
+
|
|
378
|
+
if (handleInput instanceof HTMLInputElement && handleInput.value.trim()) {
|
|
379
|
+
return handleInput.value.trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return form.getAttribute('data-formie-handle')?.trim() || '';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildHydrateFormData(field: HTMLElement, input: HTMLInputElement, assetIds: number[]): FormData {
|
|
386
|
+
const form = input.form;
|
|
387
|
+
const body = new FormData();
|
|
388
|
+
const handle = getFormHandle(form);
|
|
389
|
+
const fieldHandle = getFieldHandle(field);
|
|
390
|
+
|
|
391
|
+
if (handle) {
|
|
392
|
+
body.append('handle', handle);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (fieldHandle) {
|
|
396
|
+
body.append('fieldHandle', fieldHandle);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const submissionUidInput = form?.querySelector('input[name="submissionUid"]');
|
|
400
|
+
|
|
401
|
+
if (submissionUidInput instanceof HTMLInputElement && submissionUidInput.value.trim()) {
|
|
402
|
+
body.append('submissionUid', submissionUidInput.value.trim());
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
assetIds.forEach((assetId) => {
|
|
406
|
+
body.append('assetIds[]', String(assetId));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return body;
|
|
410
|
+
}
|
|
411
|
+
|
|
369
412
|
async function hydrateUploadedAssets(field: HTMLElement, input: HTMLInputElement, uploadedAssets: UploadedAsset[]): Promise<UploadedAsset[]> {
|
|
370
413
|
const assetIds = uploadedAssets.map((asset) => {
|
|
371
414
|
return asset.assetId;
|
|
@@ -378,14 +421,9 @@ async function hydrateUploadedAssets(field: HTMLElement, input: HTMLInputElement
|
|
|
378
421
|
}
|
|
379
422
|
|
|
380
423
|
const hydrateEndpoint = input.getAttribute(HYDRATE_ENDPOINT_ATTR)?.trim() || '/actions/formie/file-upload/hydrate';
|
|
381
|
-
const body = new FormData();
|
|
382
|
-
assetIds.forEach((assetId) => {
|
|
383
|
-
body.append('assetIds[]', String(assetId));
|
|
384
|
-
});
|
|
385
|
-
|
|
386
424
|
const response = await requestJson<FileUploadHydrateResponse>(hydrateEndpoint, {
|
|
387
425
|
method: 'POST',
|
|
388
|
-
body,
|
|
426
|
+
body: buildHydrateFormData(field, input, assetIds),
|
|
389
427
|
});
|
|
390
428
|
const hydratedAssets = normalizeUploadedAssets(response.assets);
|
|
391
429
|
|
|
@@ -504,6 +542,118 @@ function getUploadedAssetsFromEvent(event: Event, matcher: FileUploadEventMatche
|
|
|
504
542
|
return [];
|
|
505
543
|
}
|
|
506
544
|
|
|
545
|
+
function getFieldContainer(input: HTMLInputElement): HTMLElement | null {
|
|
546
|
+
const field = input.closest(FIELD_HANDLE_SELECTOR);
|
|
547
|
+
|
|
548
|
+
return field instanceof HTMLElement ? field : null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function collectFileUploadInputs(root: ParentNode): HTMLInputElement[] {
|
|
552
|
+
return Array.from(root.querySelectorAll(INPUT_SELECTOR)).filter((input): input is HTMLInputElement => {
|
|
553
|
+
return input instanceof HTMLInputElement;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function bindFileUploadInput(field: HTMLElement, input: HTMLInputElement, form: HTMLFormElement | null): () => void {
|
|
558
|
+
const state: FileUploadState = {
|
|
559
|
+
field,
|
|
560
|
+
input,
|
|
561
|
+
summaryRoot: ensureSummaryRoot(field, input),
|
|
562
|
+
uploadedAssets: mergeUploadedAssets(
|
|
563
|
+
readUploadedAssetsFromHiddenInputs(field, input),
|
|
564
|
+
readSummaryItems(field.querySelector('[data-formie-file-summary]') as HTMLElement | null),
|
|
565
|
+
),
|
|
566
|
+
pendingFiles: [],
|
|
567
|
+
hydrationToken: 0,
|
|
568
|
+
};
|
|
569
|
+
const syncPendingFiles = () => {
|
|
570
|
+
state.pendingFiles = Array.from(input.files || []).map((file) => {
|
|
571
|
+
return file.name;
|
|
572
|
+
});
|
|
573
|
+
renderSummary(state);
|
|
574
|
+
};
|
|
575
|
+
const syncUploadedAssets = async(uploadedAssets: UploadedAsset[]) => {
|
|
576
|
+
state.hydrationToken += 1;
|
|
577
|
+
const hydrationToken = state.hydrationToken;
|
|
578
|
+
state.pendingFiles = [];
|
|
579
|
+
state.uploadedAssets = uploadedAssets;
|
|
580
|
+
updateUploadedAssetInputs(field, input, uploadedAssets);
|
|
581
|
+
renderSummary(state);
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const hydratedAssets = await hydrateUploadedAssets(field, input, uploadedAssets);
|
|
585
|
+
|
|
586
|
+
if (hydrationToken !== state.hydrationToken) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
state.uploadedAssets = hydratedAssets;
|
|
591
|
+
renderSummary(state);
|
|
592
|
+
dispatchFieldEvent(field, MODULE_ID, 'uploaded-assets-sync', {
|
|
593
|
+
fileUpload: field,
|
|
594
|
+
assets: hydratedAssets,
|
|
595
|
+
});
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error('[formie] Failed to hydrate uploaded file details.', error);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const resetUploadedAssets = () => {
|
|
601
|
+
state.hydrationToken += 1;
|
|
602
|
+
state.pendingFiles = [];
|
|
603
|
+
state.uploadedAssets = [];
|
|
604
|
+
updateUploadedAssetInputs(field, input, []);
|
|
605
|
+
renderSummary(state);
|
|
606
|
+
};
|
|
607
|
+
const uploadedAssetHandler = (event: Event) => {
|
|
608
|
+
const fieldHandle = getFieldHandle(field);
|
|
609
|
+
|
|
610
|
+
if (!fieldHandle) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const uploadedAssets = getUploadedAssetsFromEvent(event, {
|
|
615
|
+
fieldHandle,
|
|
616
|
+
inputKey: getFileUploadKey(input),
|
|
617
|
+
inputName: input.name,
|
|
618
|
+
pendingFiles: state.pendingFiles,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
if (!uploadedAssets.length) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
void syncUploadedAssets(uploadedAssets);
|
|
626
|
+
};
|
|
627
|
+
const resetHandler = () => {
|
|
628
|
+
resetUploadedAssets();
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
input.addEventListener('change', syncPendingFiles);
|
|
632
|
+
form?.addEventListener(FILE_UPLOAD_EVENT, uploadedAssetHandler as EventListener);
|
|
633
|
+
form?.addEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
634
|
+
|
|
635
|
+
const syncInitialState = () => {
|
|
636
|
+
const assetIdsFromDom = readUploadedAssetsFromHiddenInputs(field, input);
|
|
637
|
+
const summaryItems = readSummaryItems(state.summaryRoot);
|
|
638
|
+
state.uploadedAssets = mergeUploadedAssets(assetIdsFromDom, summaryItems);
|
|
639
|
+
|
|
640
|
+
if (state.uploadedAssets.some((asset) => asset.assetId && !asset.filename)) {
|
|
641
|
+
void syncUploadedAssets(state.uploadedAssets);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
renderSummary(state);
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
syncInitialState();
|
|
649
|
+
|
|
650
|
+
return () => {
|
|
651
|
+
input.removeEventListener('change', syncPendingFiles);
|
|
652
|
+
form?.removeEventListener(FILE_UPLOAD_EVENT, uploadedAssetHandler as EventListener);
|
|
653
|
+
form?.removeEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
507
657
|
export const fileUploadModule: FormieModuleDefinition = {
|
|
508
658
|
id: MODULE_ID,
|
|
509
659
|
kind: 'field',
|
|
@@ -511,111 +661,53 @@ export const fileUploadModule: FormieModuleDefinition = {
|
|
|
511
661
|
return !!ctx.target.querySelector(INPUT_SELECTOR);
|
|
512
662
|
},
|
|
513
663
|
setup: async(ctx) => {
|
|
514
|
-
const field = getModuleFieldTarget(ctx);
|
|
515
|
-
const scope = field || ctx.target;
|
|
516
|
-
const inputs = Array.from(scope.querySelectorAll(INPUT_SELECTOR)).filter((input): input is HTMLInputElement => {
|
|
517
|
-
return input instanceof HTMLInputElement;
|
|
518
|
-
});
|
|
519
664
|
const form = ctx.form;
|
|
665
|
+
const boundInputs = new WeakSet<HTMLInputElement>();
|
|
666
|
+
const unbinds: Array<() => void> = [];
|
|
520
667
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!(field instanceof HTMLElement)) {
|
|
525
|
-
return () => {};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const state: FileUploadState = {
|
|
529
|
-
field,
|
|
530
|
-
input,
|
|
531
|
-
summaryRoot: ensureSummaryRoot(field, input),
|
|
532
|
-
uploadedAssets: mergeUploadedAssets(
|
|
533
|
-
readUploadedAssetsFromHiddenInputs(field, input),
|
|
534
|
-
readSummaryItems(field.querySelector('[data-formie-file-summary]') as HTMLElement | null)
|
|
535
|
-
),
|
|
536
|
-
pendingFiles: [],
|
|
537
|
-
hydrationToken: 0,
|
|
538
|
-
};
|
|
539
|
-
const syncPendingFiles = () => {
|
|
540
|
-
state.pendingFiles = Array.from(input.files || []).map((file) => {
|
|
541
|
-
return file.name;
|
|
542
|
-
});
|
|
543
|
-
renderSummary(state);
|
|
544
|
-
};
|
|
545
|
-
const syncUploadedAssets = async(uploadedAssets: UploadedAsset[]) => {
|
|
546
|
-
state.hydrationToken += 1;
|
|
547
|
-
const hydrationToken = state.hydrationToken;
|
|
548
|
-
state.pendingFiles = [];
|
|
549
|
-
state.uploadedAssets = uploadedAssets;
|
|
550
|
-
updateUploadedAssetInputs(field, input, uploadedAssets);
|
|
551
|
-
renderSummary(state);
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const hydratedAssets = await hydrateUploadedAssets(field, input, uploadedAssets);
|
|
555
|
-
|
|
556
|
-
if (hydrationToken !== state.hydrationToken) {
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
state.uploadedAssets = hydratedAssets;
|
|
561
|
-
renderSummary(state);
|
|
562
|
-
dispatchFieldEvent(field, MODULE_ID, 'uploaded-assets-sync', {
|
|
563
|
-
fileUpload: field,
|
|
564
|
-
assets: hydratedAssets,
|
|
565
|
-
});
|
|
566
|
-
} catch (error) {
|
|
567
|
-
console.error('[formie] Failed to hydrate uploaded file details.', error);
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
const resetUploadedAssets = () => {
|
|
571
|
-
state.hydrationToken += 1;
|
|
572
|
-
state.pendingFiles = [];
|
|
573
|
-
state.uploadedAssets = [];
|
|
574
|
-
updateUploadedAssetInputs(field, input, []);
|
|
575
|
-
renderSummary(state);
|
|
576
|
-
};
|
|
577
|
-
const uploadedAssetHandler = (event: Event) => {
|
|
578
|
-
const fieldHandle = getFieldHandle(field);
|
|
579
|
-
|
|
580
|
-
if (!fieldHandle) {
|
|
668
|
+
const bindInputsInRoot = (root: ParentNode) => {
|
|
669
|
+
collectFileUploadInputs(root).forEach((input) => {
|
|
670
|
+
if (boundInputs.has(input)) {
|
|
581
671
|
return;
|
|
582
672
|
}
|
|
583
673
|
|
|
584
|
-
const
|
|
585
|
-
fieldHandle,
|
|
586
|
-
inputKey: getFileUploadKey(input),
|
|
587
|
-
inputName: input.name,
|
|
588
|
-
pendingFiles: state.pendingFiles,
|
|
589
|
-
});
|
|
674
|
+
const field = getFieldContainer(input);
|
|
590
675
|
|
|
591
|
-
if (!
|
|
676
|
+
if (!field) {
|
|
592
677
|
return;
|
|
593
678
|
}
|
|
594
679
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
680
|
+
boundInputs.add(input);
|
|
681
|
+
unbinds.push(bindFileUploadInput(field, input, form));
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
registerValidators(form);
|
|
686
|
+
bindInputsInRoot(ctx.target);
|
|
600
687
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
form?.addEventListener(FORM_RESET_EVENT, resetHandler as EventListener);
|
|
604
|
-
renderSummary(state);
|
|
688
|
+
const repeaterInitRowHandler = (event: Event) => {
|
|
689
|
+
const detail = (event as CustomEvent<unknown>).detail;
|
|
605
690
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
691
|
+
if (!isRecord(detail)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const row = detail.row;
|
|
696
|
+
|
|
697
|
+
if (row instanceof HTMLElement) {
|
|
698
|
+
bindInputsInRoot(row);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
form?.addEventListener(REPEATER_INIT_ROW_EVENT, repeaterInitRowHandler as EventListener);
|
|
612
703
|
|
|
613
704
|
await ctx.emit('formie:module:file-upload:init', {
|
|
614
|
-
count:
|
|
705
|
+
count: unbinds.length,
|
|
615
706
|
});
|
|
616
707
|
|
|
617
708
|
return {
|
|
618
709
|
destroy: () => {
|
|
710
|
+
form?.removeEventListener(REPEATER_INIT_ROW_EVENT, repeaterInitRowHandler as EventListener);
|
|
619
711
|
unbinds.forEach((unbind) => {
|
|
620
712
|
unbind();
|
|
621
713
|
});
|
|
@@ -3,6 +3,7 @@ import type { FormSubmitResult } from '#contracts/schema';
|
|
|
3
3
|
import { EventBus } from '#events/event-bus';
|
|
4
4
|
import { submitForm } from '#transport/forms-api';
|
|
5
5
|
import { createDebug } from '#utils/debug';
|
|
6
|
+
import { getFormPages, getValidationScope } from '#validation/scope';
|
|
6
7
|
import type { FormieValidator } from '#validation/validator';
|
|
7
8
|
|
|
8
9
|
export type SubmitPipelineContext = {
|
|
@@ -30,32 +31,6 @@ function getAbortedResult(stage: SubmitStage, reason?: string): FormSubmitResult
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function getPages(form: HTMLFormElement): HTMLElement[] {
|
|
34
|
-
return Array.from(form.querySelectorAll('[data-formie-page]')) as HTMLElement[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getValidationScope(form: HTMLFormElement): { scope: Element; final: boolean } {
|
|
38
|
-
const pages = getPages(form);
|
|
39
|
-
|
|
40
|
-
if (!pages.length) {
|
|
41
|
-
return {
|
|
42
|
-
scope: form,
|
|
43
|
-
final: true,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Multi-page submits validate the visible page until the user reaches the
|
|
48
|
-
// final step, where hidden earlier pages are included again.
|
|
49
|
-
const currentPage = pages.find((page) => {
|
|
50
|
-
return !page.hasAttribute('data-formie-page-hidden');
|
|
51
|
-
}) || pages[pages.length - 1];
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
scope: currentPage,
|
|
55
|
-
final: currentPage === pages[pages.length - 1],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
34
|
function isSubmittableControl(element: Element): element is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement {
|
|
60
35
|
return element instanceof HTMLInputElement
|
|
61
36
|
|| element instanceof HTMLSelectElement
|
|
@@ -172,7 +147,7 @@ function appendMissingFieldClears(formData: FormData, fieldNames: Set<string>):
|
|
|
172
147
|
}
|
|
173
148
|
|
|
174
149
|
function buildSubmitFormData(form: HTMLFormElement, action: FormAction): FormData {
|
|
175
|
-
const pages =
|
|
150
|
+
const pages = getFormPages(form);
|
|
176
151
|
const currentPage = pages.find((page) => {
|
|
177
152
|
return !page.hasAttribute('data-formie-page-hidden');
|
|
178
153
|
}) || null;
|
|
@@ -200,7 +175,7 @@ function isFinalSubmitAttempt(form: HTMLFormElement, action: FormAction): boolea
|
|
|
200
175
|
return false;
|
|
201
176
|
}
|
|
202
177
|
|
|
203
|
-
const pages =
|
|
178
|
+
const pages = getFormPages(form);
|
|
204
179
|
|
|
205
180
|
if (!pages.length) {
|
|
206
181
|
return true;
|