@verbb/formie-browser 1.0.14 → 1.0.16
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 +66 -0
- package/dist/chunks/{address-country-Dn71jroa.js → address-country-DLJF-OkC.js} +5 -9
- package/dist/chunks/{address-finder-O49uDRoR.js → address-finder-QK1m1eyv.js} +2 -2
- package/dist/chunks/{address-state-ByYatUur.js → address-state-CHKit8AW.js} +113 -112
- package/dist/chunks/{api-DYYsVIub.js → api-BPtH5RBA.js} +162 -130
- package/dist/chunks/{api-D6kJ7kXZ.js → api-BR2Gqy37.js} +313 -303
- package/dist/chunks/{api-ghNr2uxX.js → api-CmuOZ0i3.js} +1 -1
- package/dist/chunks/{async-nPFRNQ06.js → async-2GdXNFCE.js} +18 -1
- package/dist/chunks/{bpoint-D0RKFM4F.js → bpoint-GvYfyPjD.js} +1 -1
- package/dist/chunks/{calculations-YEoVCG2q.js → calculations-DeEmdUDq.js} +5 -7
- package/dist/chunks/{captcha-eu-DERJbXnH.js → captcha-eu-eU_M6snp.js} +2 -2
- package/dist/chunks/{checkbox-radio-gsm8QPJg.js → checkbox-radio-CSXo6svm.js} +4 -12
- package/dist/chunks/{combobox-CWNdx4Fk.js → combobox-CJh2DoSO.js} +323 -363
- package/dist/chunks/{conditions-DqNHLTbN.js → conditions-Dz3068K0.js} +140 -153
- package/dist/chunks/{country-from-ip-DDj_mFki.js → country-from-ip-BqQ-BqLH.js} +1 -1
- package/dist/chunks/{custom-google-maps-C4l2S3GO.js → custom-google-maps-rNm5VFbb.js} +2 -6
- package/dist/chunks/{custom-link-CLazgKYH.js → custom-link-CLqFCssQ.js} +1 -1
- package/dist/chunks/{custom-maps-DqcAlD0v.js → custom-maps-DgjYQDLj.js} +4 -8
- package/dist/chunks/{date-picker-RUnevF7l.js → date-picker-BVJxJVYT.js} +42 -58
- package/dist/chunks/{debug-BV0DvdHx.js → debug-rhzqP1j2.js} +2 -14
- package/dist/chunks/{dist-ZLeW0zZ3.js → dist-CJReDlDK.js} +13 -8
- package/dist/chunks/{eway-or6dtjoT.js → eway-BZuTBAUW.js} +3 -3
- package/dist/chunks/{field-references.resolver-Ca8MguFv.js → field-references.resolver-uSHf5-NP.js} +1 -1
- package/dist/chunks/{file-upload-Bm-TXyNB.js → file-upload-vEUr_EuP.js} +131 -113
- package/dist/chunks/{friendly-captcha-v1-GFqCt1Mh.js → friendly-captcha-v1-C_cF1WD2.js} +2 -2
- package/dist/chunks/{friendly-captcha-v2-WoW246JG.js → friendly-captcha-v2-BenESH-n.js} +2 -2
- package/dist/chunks/{friendly-challenge-DPG9MJb1.js → friendly-challenge-BgPBEMXx.js} +21 -35
- package/dist/chunks/{go-cardless-DzJ-0FKg.js → go-cardless-DWZeB0Bq.js} +2 -6
- package/dist/chunks/{google-address-C1P1nTDM.js → google-address-DQR6rsbh.js} +15 -10
- package/dist/chunks/{hcaptcha-D0T8aIHz.js → hcaptcha-COilLmRT.js} +2 -2
- package/dist/chunks/{hidden-BZhXnNPe.js → hidden-Bc2TpcgF.js} +1 -1
- package/dist/chunks/{http-C2QdHuWO.js → http-Dj_skqaa.js} +1 -1
- package/dist/chunks/{leaflet-src-DE6rv6M-.js → leaflet-src-Cg-LUOD8.js} +74 -122
- package/dist/chunks/{loqate-DD1S7PY9.js → loqate-ZZClcacr.js} +2 -2
- package/dist/chunks/{mollie-DG8Ju83x.js → mollie-8OMXIYa8.js} +2 -6
- package/dist/chunks/{moneris-C8m3Fvqu.js → moneris-qYHJ9EFy.js} +1 -1
- package/dist/chunks/opayo-DlWPEnla.js +159 -0
- package/dist/chunks/{paddle-BIYR9Um3.js → paddle-CArfWih0.js} +2 -2
- package/dist/chunks/{password-validation-C8_uO_c_.js → password-validation-HGOU9kf3.js} +2 -2
- package/dist/chunks/{paypal-B_yxIo2d.js → paypal-BcaMCMGY.js} +2 -2
- package/dist/chunks/{payway-CeOzS0sL.js → payway-vxC7_bfR.js} +4 -12
- package/dist/chunks/{phone-country-bAqje17P.js → phone-country-Bx9EG50h.js} +124 -141
- package/dist/chunks/{place-kit-BE5gi3vo.js → place-kit-BKk8xDXf.js} +10 -10
- package/dist/chunks/{placekit-autocomplete.esm-CZZERYur.js → placekit-autocomplete.esm-D2A_0iVU.js} +2 -5
- package/dist/chunks/{recaptcha-enterprise-CruXddZq.js → recaptcha-enterprise-Cx6HUpMa.js} +6 -12
- package/dist/chunks/recaptcha-shared-CcMI-r8t.js +33 -0
- package/dist/chunks/{recaptcha-v2-checkbox-CCnJYCnG.js → recaptcha-v2-checkbox-DQ9BJzcv.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-BuVqRRYn.js → recaptcha-v2-invisible-BP0T8Pan.js} +2 -2
- package/dist/chunks/recaptcha-v3-DKMGQ27K.js +38 -0
- package/dist/chunks/{repeater-DwE27jI1.js → repeater-BR0RUvTs.js} +50 -40
- package/dist/chunks/{rich-text-uV4o_pGk.js → rich-text-H2A6cm1t.js} +33 -11
- package/dist/chunks/{rolldown-runtime-DtPi1Y-2.js → rolldown-runtime-VHNaYcLd.js} +1 -1
- package/dist/chunks/{scripts-CbQ7agX3.js → scripts-DrccjrFv.js} +1 -1
- package/dist/chunks/{sdk-lkwSvlpM.js → sdk-_cmrRYgp.js} +4 -16
- package/dist/chunks/{shared-OKhSay59.js → shared-CJ1LIEh3.js} +1 -3
- package/dist/chunks/{signature-7cldFbqj.js → signature-CfKA_poI.js} +9 -41
- package/dist/chunks/skip-CEW89ExY.js +7 -0
- package/dist/chunks/{snaptcha-C04kNGYQ.js → snaptcha-P3EoSQud.js} +1 -1
- package/dist/chunks/{square-DEzxv9D9.js → square-Dyz0p_GU.js} +2 -2
- package/dist/chunks/{stripe-CVPQGDz3.js → stripe-Bfd_jp_k.js} +4 -7
- package/dist/chunks/{summary-BF_8mrXI.js → summary-B3CAebLV.js} +45 -37
- package/dist/chunks/{survey-likert-fk1hUPff.js → survey-likert-CEtUceag.js} +1 -1
- package/dist/chunks/{survey-rank-BsLGl2LX.js → survey-rank-Dtt97y6s.js} +3 -7
- package/dist/chunks/{survey-rating-DVS6e6fM.js → survey-rating-mlDMI0uq.js} +9 -23
- package/dist/chunks/{table-DnIBdeKF.js → table-0k6Md0IO.js} +2 -2
- package/dist/chunks/{text-limit-LGMPhFuC.js → text-limit-CTVML6s7.js} +10 -10
- package/dist/chunks/{turnstile-n9DNyPxi.js → turnstile-Sp4ubowK.js} +2 -2
- package/dist/chunks/{upload-manager-BbiUosZq.js → upload-manager-Ccg3d_4I.js} +258 -260
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +743 -734
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/dom-events.d.ts +5 -0
- package/dist/js/core/dom-events.d.ts.map +1 -1
- package/dist/js/core/page-client-event.d.ts.map +1 -1
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts +3 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/core/validation-focus.d.ts.map +1 -1
- package/dist/js/modules/address/google-address.d.ts +10 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/address/place-kit.d.ts.map +1 -1
- package/dist/js/modules/captchas/factories.d.ts.map +1 -1
- package/dist/js/modules/captchas/host.d.ts +4 -1
- package/dist/js/modules/captchas/host.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
- package/dist/js/modules/fields/address-state.d.ts.map +1 -1
- package/dist/js/modules/fields/combobox.d.ts +2 -0
- package/dist/js/modules/fields/combobox.d.ts.map +1 -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/date-picker.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
- package/dist/js/modules/fields/repeater.d.ts.map +1 -1
- package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
- package/dist/js/modules/fields/summary.d.ts.map +1 -1
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
- package/dist/js/modules/fields/upload-manager.d.ts.map +1 -1
- package/dist/js/modules/payments/factories.d.ts.map +1 -1
- package/dist/js/modules/payments/opayo.d.ts.map +1 -1
- package/dist/js/submit/pipeline.d.ts +2 -0
- package/dist/js/submit/pipeline.d.ts.map +1 -1
- package/dist/js/utils/async.d.ts +1 -0
- package/dist/js/utils/async.d.ts.map +1 -1
- package/dist/js/utils/form-started-at.d.ts +1 -0
- package/dist/js/utils/form-started-at.d.ts.map +1 -1
- package/dist/js/utils/unload-warning.d.ts +4 -0
- package/dist/js/utils/unload-warning.d.ts.map +1 -1
- package/dist/js/validation/rules/required.d.ts.map +1 -1
- package/dist/js/validation/submit-readiness.d.ts.map +1 -1
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/js/core/create-formie-client.ts +37 -28
- package/src/js/core/dom-events.ts +30 -1
- package/src/js/core/page-client-event.ts +5 -7
- package/src/js/core/submit-result-state.ts +30 -2
- package/src/js/core/submit-result-ui.ts +25 -3
- package/src/js/core/validation-focus.ts +17 -3
- package/src/js/modules/address/google-address.ts +95 -37
- package/src/js/modules/address/place-kit.ts +13 -7
- package/src/js/modules/captchas/factories.ts +241 -101
- package/src/js/modules/captchas/host.ts +16 -0
- package/src/js/modules/captchas/recaptcha-enterprise.ts +1 -1
- package/src/js/modules/captchas/recaptcha-shared.ts +23 -5
- package/src/js/modules/captchas/recaptcha-v3.ts +23 -14
- package/src/js/modules/fields/address-country.ts +5 -5
- package/src/js/modules/fields/address-state.ts +80 -27
- package/src/js/modules/fields/combobox.ts +22 -1
- package/src/js/modules/fields/conditions/effects.ts +4 -5
- package/src/js/modules/fields/conditions.ts +9 -0
- package/src/js/modules/fields/date-picker.ts +6 -3
- package/src/js/modules/fields/file-upload.ts +55 -18
- package/src/js/modules/fields/phone-country.ts +25 -2
- package/src/js/modules/fields/repeater.ts +55 -4
- package/src/js/modules/fields/rich-text.ts +33 -0
- package/src/js/modules/fields/summary.ts +63 -10
- package/src/js/modules/fields/text-limit.ts +11 -0
- package/src/js/modules/fields/upload-manager.ts +54 -18
- package/src/js/modules/payments/factories.ts +87 -54
- package/src/js/modules/payments/opayo.ts +7 -0
- package/src/js/submit/pipeline.ts +16 -9
- package/src/js/utils/async.ts +34 -0
- package/src/js/utils/form-started-at.ts +19 -4
- package/src/js/utils/unload-warning.ts +63 -7
- package/src/js/validation/rules/required.ts +18 -0
- package/src/js/validation/submit-readiness.ts +2 -0
- package/src/js/validation/validator.ts +29 -6
- package/src/vendor.d.ts +2 -0
- package/dist/chunks/opayo-B3EvrVxw.js +0 -188
- package/dist/chunks/recaptcha-shared-AQRMDMme.js +0 -27
- package/dist/chunks/recaptcha-v3-CloqhO2A.js +0 -28
|
@@ -132,6 +132,9 @@ export function createManagedPaymentModule<
|
|
|
132
132
|
const cleanups: Cleanup[] = [];
|
|
133
133
|
let apiPromise: Promise<TApi> | null = null;
|
|
134
134
|
let widget: TWidget | null = null;
|
|
135
|
+
let mountPromise: Promise<void> | null = null;
|
|
136
|
+
let destructionPromise: Promise<void> | null = null;
|
|
137
|
+
let destroyed = false;
|
|
135
138
|
let customSetupResult: PaymentSetupResult | null = null;
|
|
136
139
|
let authorizeInFlight: Promise<boolean> | null = null;
|
|
137
140
|
|
|
@@ -145,32 +148,59 @@ export function createManagedPaymentModule<
|
|
|
145
148
|
};
|
|
146
149
|
|
|
147
150
|
const ensureMounted = async() => {
|
|
148
|
-
|
|
151
|
+
const mount = adapter.mount;
|
|
152
|
+
|
|
153
|
+
if (destroyed || !mount || widget || !isPaymentFieldActive(ctx.target)) {
|
|
149
154
|
return;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
// Conditions can fire again while a provider is requesting its session.
|
|
158
|
+
// Share the whole mount, including API loading, with submission callers.
|
|
159
|
+
if (!mountPromise) {
|
|
160
|
+
mountPromise = (async() => {
|
|
161
|
+
try {
|
|
162
|
+
const api = await getApi();
|
|
163
|
+
|
|
164
|
+
if (destroyed || !isPaymentFieldActive(ctx.target)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
services.removeError();
|
|
169
|
+
widget = await mount({
|
|
170
|
+
api,
|
|
171
|
+
field: ctx.target,
|
|
172
|
+
services,
|
|
173
|
+
options,
|
|
174
|
+
provider: options.provider,
|
|
175
|
+
});
|
|
176
|
+
debug.log('Payment widget mounted.', {
|
|
177
|
+
moduleId: adapter.moduleId,
|
|
178
|
+
handle: options.handle,
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Setup and submission callers still receive provider failures.
|
|
182
|
+
debug.warn('Payment widget mount failed.', {
|
|
183
|
+
moduleId: adapter.moduleId,
|
|
184
|
+
handle: options.handle,
|
|
185
|
+
});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
})().finally(() => {
|
|
189
|
+
mountPromise = null;
|
|
171
190
|
});
|
|
172
|
-
throw error;
|
|
173
191
|
}
|
|
192
|
+
|
|
193
|
+
return mountPromise;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleVisibility = () => {
|
|
197
|
+
// Event listeners have no awaited caller to handle a rejection.
|
|
198
|
+
// Allow the next visibility or submission attempt to retry the mount.
|
|
199
|
+
void ensureMounted().catch(() => {
|
|
200
|
+
if (!destroyed && isPaymentFieldActive(ctx.target)) {
|
|
201
|
+
services.addError(options.transport.errorMessage);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
174
204
|
};
|
|
175
205
|
|
|
176
206
|
// Clear stale payment feedback at the start of every submit attempt.
|
|
@@ -194,10 +224,6 @@ export function createManagedPaymentModule<
|
|
|
194
224
|
// Remount when page flow or field conditions reveal this payment field.
|
|
195
225
|
const visibilityEvents = ['formie:page:navigate:after', 'formie:submit:result'];
|
|
196
226
|
visibilityEvents.forEach((eventName) => {
|
|
197
|
-
const handleVisibility = () => {
|
|
198
|
-
void ensureMounted();
|
|
199
|
-
};
|
|
200
|
-
|
|
201
227
|
ctx.root.addEventListener(eventName, handleVisibility as EventListener);
|
|
202
228
|
cleanups.push(() => {
|
|
203
229
|
ctx.root.removeEventListener(eventName, handleVisibility as EventListener);
|
|
@@ -205,41 +231,48 @@ export function createManagedPaymentModule<
|
|
|
205
231
|
});
|
|
206
232
|
|
|
207
233
|
// Conditions emit on the module bus (not only as page-nav DOM events).
|
|
208
|
-
cleanups.push(ctx.on('formie:conditions:evaluated',
|
|
209
|
-
void ensureMounted();
|
|
210
|
-
}));
|
|
234
|
+
cleanups.push(ctx.on('formie:conditions:evaluated', handleVisibility));
|
|
211
235
|
|
|
212
|
-
const destroy =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
const destroy = (): Promise<void> => {
|
|
237
|
+
if (destructionPromise) return destructionPromise;
|
|
238
|
+
destroyed = true;
|
|
239
|
+
destructionPromise = (async() => {
|
|
240
|
+
debug.log('Destroying payment module.', {
|
|
241
|
+
moduleId: adapter.moduleId,
|
|
242
|
+
handle: options.handle,
|
|
243
|
+
});
|
|
244
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
218
245
|
|
|
219
|
-
|
|
220
|
-
|
|
246
|
+
// A session request may finish after teardown starts. Release the
|
|
247
|
+
// resulting widget before another instance can use this container.
|
|
248
|
+
await mountPromise?.catch(() => {});
|
|
249
|
+
const mountedWidget = widget;
|
|
250
|
+
widget = null;
|
|
221
251
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
252
|
+
try {
|
|
253
|
+
if (mountedWidget && adapter.unmount) {
|
|
254
|
+
const api = await getApi();
|
|
255
|
+
|
|
256
|
+
await adapter.unmount({
|
|
257
|
+
api,
|
|
258
|
+
widget: mountedWidget,
|
|
259
|
+
field: ctx.target,
|
|
260
|
+
services,
|
|
261
|
+
options,
|
|
262
|
+
provider: options.provider,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
if (registry[occurrenceKey]?.destroy === destroy) {
|
|
267
|
+
delete registry[occurrenceKey];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
debug.log('Payment module destroy complete.', {
|
|
231
271
|
moduleId: adapter.moduleId,
|
|
232
272
|
handle: options.handle,
|
|
233
273
|
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (registry[occurrenceKey]?.destroy === destroy) {
|
|
237
|
-
delete registry[occurrenceKey];
|
|
238
|
-
}
|
|
239
|
-
debug.log('Payment module destroy complete.', {
|
|
240
|
-
moduleId: adapter.moduleId,
|
|
241
|
-
handle: options.handle,
|
|
242
|
-
});
|
|
274
|
+
})();
|
|
275
|
+
return destructionPromise;
|
|
243
276
|
};
|
|
244
277
|
|
|
245
278
|
registry[occurrenceKey] = { destroy };
|
|
@@ -3,6 +3,7 @@ import { browserRequest, type BrowserRequestOptions } from '@verbb/formie-core';
|
|
|
3
3
|
import opayoCss from '#theme-css/integrations/_opayo.css?inline';
|
|
4
4
|
|
|
5
5
|
import { definePaymentModule } from '#modules/payments/api';
|
|
6
|
+
import { findPaymentInputBySuffix } from '#modules/payments/utils';
|
|
6
7
|
import { ensureModuleStyles } from '#modules/styles';
|
|
7
8
|
import { toggleThemeClasses } from '#theme/theme-classes';
|
|
8
9
|
import { createDebug } from '#utils/debug';
|
|
@@ -277,6 +278,12 @@ export const opayoModule = definePaymentModule<OpayoProviderOptions, null, Opayo
|
|
|
277
278
|
return false;
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
// A completed challenge resumes the original transaction. The server
|
|
282
|
+
// verifies this reference and its ownership; no new card token is needed.
|
|
283
|
+
if (findPaymentInputBySuffix(field, 'opayo3DSComplete')?.value.trim()) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
280
287
|
if (isDropInCheckoutMode(provider)) {
|
|
281
288
|
if (!widget) {
|
|
282
289
|
services.addError('Opayo drop-in checkout is not ready.');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { FormieFormInstance } from '#contracts/client';
|
|
1
2
|
import type { FormAction, SubmitStage } from '#contracts/common';
|
|
2
3
|
import type { FormSubmitResult } from '#contracts/schema';
|
|
4
|
+
import { withFormEventInstance } from '#core/dom-events';
|
|
3
5
|
import { EventBus } from '#events/event-bus';
|
|
4
6
|
import { submitForm } from '#transport/forms-api';
|
|
5
7
|
import { createDebug } from '#utils/debug';
|
|
@@ -7,6 +9,7 @@ import { getFormPages, getValidationScope } from '#validation/scope';
|
|
|
7
9
|
import type { FormieValidator } from '#validation/validator';
|
|
8
10
|
|
|
9
11
|
export type SubmitPipelineContext = {
|
|
12
|
+
instance?: FormieFormInstance;
|
|
10
13
|
form: HTMLFormElement;
|
|
11
14
|
action: FormAction;
|
|
12
15
|
formData: FormData;
|
|
@@ -207,7 +210,7 @@ export async function runSubmitPipeline(
|
|
|
207
210
|
let dispatchResult: FormSubmitResult | null = null;
|
|
208
211
|
const finalSubmitAttempt = isFinalSubmitAttempt(form, action);
|
|
209
212
|
|
|
210
|
-
const context: SubmitPipelineContext = {
|
|
213
|
+
const context: SubmitPipelineContext = withFormEventInstance(form, {
|
|
211
214
|
form,
|
|
212
215
|
action,
|
|
213
216
|
formData: buildSubmitFormData(form, action),
|
|
@@ -218,6 +221,10 @@ export async function runSubmitPipeline(
|
|
|
218
221
|
},
|
|
219
222
|
isAborted: () => aborted,
|
|
220
223
|
abortReason: () => abortReason,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const emitSubmitResult = (eventName: string, result: FormSubmitResult) => {
|
|
227
|
+
return bus.emitSafe(eventName, withFormEventInstance(form, result));
|
|
221
228
|
};
|
|
222
229
|
|
|
223
230
|
const runners: Record<SubmitStage, StageRunner> = {
|
|
@@ -368,7 +375,7 @@ export async function runSubmitPipeline(
|
|
|
368
375
|
if (aborted) {
|
|
369
376
|
const abortedResult = getAbortedResult(stage, abortReason);
|
|
370
377
|
{
|
|
371
|
-
const emitReport = await
|
|
378
|
+
const emitReport = await emitSubmitResult('formie:submit:after', abortedResult);
|
|
372
379
|
if (emitReport.failed.length > 0) {
|
|
373
380
|
debug.warn('Submit after listeners failed (abort before stage).', {
|
|
374
381
|
stage,
|
|
@@ -377,7 +384,7 @@ export async function runSubmitPipeline(
|
|
|
377
384
|
}
|
|
378
385
|
}
|
|
379
386
|
if (finalSubmitAttempt) {
|
|
380
|
-
const emitReport = await
|
|
387
|
+
const emitReport = await emitSubmitResult('formie:submit:final:after', abortedResult);
|
|
381
388
|
if (emitReport.failed.length > 0) {
|
|
382
389
|
debug.warn('Final submit after listeners failed (abort before stage).', {
|
|
383
390
|
stage,
|
|
@@ -414,7 +421,7 @@ export async function runSubmitPipeline(
|
|
|
414
421
|
if (aborted) {
|
|
415
422
|
const abortedResult = getAbortedResult(stage, abortReason);
|
|
416
423
|
{
|
|
417
|
-
const emitReport = await
|
|
424
|
+
const emitReport = await emitSubmitResult('formie:submit:after', abortedResult);
|
|
418
425
|
if (emitReport.failed.length > 0) {
|
|
419
426
|
debug.warn('Submit after listeners failed (abort after stage).', {
|
|
420
427
|
stage,
|
|
@@ -423,7 +430,7 @@ export async function runSubmitPipeline(
|
|
|
423
430
|
}
|
|
424
431
|
}
|
|
425
432
|
if (finalSubmitAttempt) {
|
|
426
|
-
const emitReport = await
|
|
433
|
+
const emitReport = await emitSubmitResult('formie:submit:final:after', abortedResult);
|
|
427
434
|
if (emitReport.failed.length > 0) {
|
|
428
435
|
debug.warn('Final submit after listeners failed (abort after stage).', {
|
|
429
436
|
stage,
|
|
@@ -439,7 +446,7 @@ export async function runSubmitPipeline(
|
|
|
439
446
|
// matching stage after-hook so modules can clean up their own state.
|
|
440
447
|
if (stageResult && !stageResult.ok) {
|
|
441
448
|
{
|
|
442
|
-
const emitReport = await
|
|
449
|
+
const emitReport = await emitSubmitResult('formie:submit:after', stageResult);
|
|
443
450
|
if (emitReport.failed.length > 0) {
|
|
444
451
|
debug.warn('Submit after listeners failed (failed stage).', {
|
|
445
452
|
stage,
|
|
@@ -448,7 +455,7 @@ export async function runSubmitPipeline(
|
|
|
448
455
|
}
|
|
449
456
|
}
|
|
450
457
|
if (finalSubmitAttempt) {
|
|
451
|
-
const emitReport = await
|
|
458
|
+
const emitReport = await emitSubmitResult('formie:submit:final:after', stageResult);
|
|
452
459
|
if (emitReport.failed.length > 0) {
|
|
453
460
|
debug.warn('Final submit after listeners failed (failed stage).', {
|
|
454
461
|
stage,
|
|
@@ -473,7 +480,7 @@ export async function runSubmitPipeline(
|
|
|
473
480
|
};
|
|
474
481
|
|
|
475
482
|
{
|
|
476
|
-
const emitReport = await
|
|
483
|
+
const emitReport = await emitSubmitResult('formie:submit:after', successResult);
|
|
477
484
|
if (emitReport.failed.length > 0) {
|
|
478
485
|
debug.warn('Submit after listeners failed (success).', {
|
|
479
486
|
failed: emitReport.failed.length,
|
|
@@ -481,7 +488,7 @@ export async function runSubmitPipeline(
|
|
|
481
488
|
}
|
|
482
489
|
}
|
|
483
490
|
if (finalSubmitAttempt) {
|
|
484
|
-
const emitReport = await
|
|
491
|
+
const emitReport = await emitSubmitResult('formie:submit:final:after', successResult);
|
|
485
492
|
if (emitReport.failed.length > 0) {
|
|
486
493
|
debug.warn('Final submit after listeners failed (success).', {
|
|
487
494
|
failed: emitReport.failed.length,
|
package/src/js/utils/async.ts
CHANGED
|
@@ -79,3 +79,37 @@ export function waitForElement(
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export function waitForElementRemoval(
|
|
84
|
+
selector: string,
|
|
85
|
+
root: ParentNode = document,
|
|
86
|
+
timeoutMs = 10000,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
if (!root.querySelector(selector)) {
|
|
90
|
+
resolve();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let timeoutId: number | null = null;
|
|
95
|
+
const observer = new MutationObserver(() => {
|
|
96
|
+
if (root.querySelector(selector)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
observer.disconnect();
|
|
101
|
+
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
102
|
+
resolve();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
observer.observe(root, {
|
|
106
|
+
childList: true,
|
|
107
|
+
subtree: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
timeoutId = window.setTimeout(() => {
|
|
111
|
+
observer.disconnect();
|
|
112
|
+
reject(new Error(`Timed out waiting for "${selector}" to be removed.`));
|
|
113
|
+
}, Math.max(timeoutMs, 0));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -11,15 +11,30 @@ export function ensureFormStartedAt(form: HTMLFormElement): void {
|
|
|
11
11
|
const renderId = renderIdInput?.value?.trim() ?? '';
|
|
12
12
|
const storageKey = renderId ? `${FORM_STARTED_AT_STORAGE_PREFIX}${renderId}` : null;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const continuing = ['submissionUid', 'progressId'].some((name) =>
|
|
15
|
+
form.querySelector<HTMLInputElement>(`input[name="${name}"]`)?.value.trim()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Reuse elapsed time for an existing draft or live mount, not a fresh visit.
|
|
19
|
+
let startedAt = startedAtInput.value || (continuing && storageKey ? sessionStorage.getItem(storageKey) : null);
|
|
15
20
|
|
|
16
21
|
if (!startedAt) {
|
|
17
22
|
startedAt = String(Date.now());
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
25
|
+
if (storageKey) {
|
|
26
|
+
sessionStorage.setItem(storageKey, startedAt);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
startedAtInput.value = startedAt;
|
|
25
30
|
}
|
|
31
|
+
|
|
32
|
+
export function resetFormStartedAt(form: HTMLFormElement): void {
|
|
33
|
+
const input = form.querySelector<HTMLInputElement>('input[name="formStartedAt"]');
|
|
34
|
+
|
|
35
|
+
if (input) {
|
|
36
|
+
// Hidden input values survive form.reset(), so start the next submission explicitly.
|
|
37
|
+
input.value = String(Date.now());
|
|
38
|
+
ensureFormStartedAt(form);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isFormCsrfFieldName } from '#utils/csrf';
|
|
2
|
+
import { dispatchFormieDomEvent } from '#core/dom-events';
|
|
2
3
|
|
|
3
4
|
const DIRTY_TRACKING_IGNORED_FIELD_NAMES = new Set([
|
|
4
5
|
'action',
|
|
@@ -14,6 +15,37 @@ const DIRTY_TRACKING_IGNORED_FIELD_NAMES = new Set([
|
|
|
14
15
|
'progressId',
|
|
15
16
|
]);
|
|
16
17
|
|
|
18
|
+
type FormSnapshot = Array<[string, FormDataEntryValue]>;
|
|
19
|
+
|
|
20
|
+
export type FormUnloadWarningExclusionsEventDetail = {
|
|
21
|
+
readonly form: HTMLFormElement;
|
|
22
|
+
readonly exclude: (...fieldNames: string[]) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function collectExcludedFieldNames(form: HTMLFormElement): Set<string> {
|
|
26
|
+
const excludedFieldNames = new Set<string>();
|
|
27
|
+
let collecting = true;
|
|
28
|
+
|
|
29
|
+
dispatchFormieDomEvent(form, 'formie:unload-warning:collect-exclusions', {
|
|
30
|
+
form,
|
|
31
|
+
exclude: (...fieldNames: string[]) => {
|
|
32
|
+
// Exclusions belong to this synchronous comparison, not a later check.
|
|
33
|
+
if (!collecting) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fieldNames.forEach((name) => {
|
|
38
|
+
if (typeof name === 'string' && name !== '') {
|
|
39
|
+
excludedFieldNames.add(name);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
} satisfies FormUnloadWarningExclusionsEventDetail);
|
|
44
|
+
collecting = false;
|
|
45
|
+
|
|
46
|
+
return excludedFieldNames;
|
|
47
|
+
}
|
|
48
|
+
|
|
17
49
|
function serializeStableValue(value: unknown, seen: WeakSet<object>): string {
|
|
18
50
|
if (value == null) {
|
|
19
51
|
return String(value);
|
|
@@ -82,9 +114,16 @@ function shouldTrackFieldName(name: string, form: HTMLFormElement): boolean {
|
|
|
82
114
|
return !DIRTY_TRACKING_IGNORED_FIELD_NAMES.has(normalizedName);
|
|
83
115
|
}
|
|
84
116
|
|
|
85
|
-
function buildTrackedSnapshot(
|
|
86
|
-
|
|
87
|
-
|
|
117
|
+
function buildTrackedSnapshot(
|
|
118
|
+
snapshot: FormSnapshot,
|
|
119
|
+
form: HTMLFormElement,
|
|
120
|
+
excludedFieldNames: Set<string>,
|
|
121
|
+
): string {
|
|
122
|
+
const entries = snapshot.filter(([name]) => {
|
|
123
|
+
return (
|
|
124
|
+
!excludedFieldNames.has(name) &&
|
|
125
|
+
shouldTrackFieldName(String(name || ''), form)
|
|
126
|
+
);
|
|
88
127
|
});
|
|
89
128
|
|
|
90
129
|
return stableSerialize(entries);
|
|
@@ -103,8 +142,9 @@ export function createFormUnloadWarningGuard(
|
|
|
103
142
|
shouldWarn?: () => boolean;
|
|
104
143
|
} = {},
|
|
105
144
|
): FormUnloadWarningGuard {
|
|
106
|
-
let baselineSnapshot:
|
|
145
|
+
let baselineSnapshot: FormSnapshot = [];
|
|
107
146
|
let isReady = false;
|
|
147
|
+
let destroyed = false;
|
|
108
148
|
let isDirty = false;
|
|
109
149
|
let animationFrameId: number | null = null;
|
|
110
150
|
let dirtyTimerId: number | null = null;
|
|
@@ -132,18 +172,31 @@ export function createFormUnloadWarningGuard(
|
|
|
132
172
|
return false;
|
|
133
173
|
}
|
|
134
174
|
|
|
135
|
-
|
|
175
|
+
// Apply the same current exclusions to both sides. A late registration
|
|
176
|
+
// must neither create a false edit nor reset genuine visitor changes.
|
|
177
|
+
const excludedFieldNames = collectExcludedFieldNames(form);
|
|
178
|
+
const currentSnapshot = Array.from(new FormData(form).entries());
|
|
179
|
+
isDirty =
|
|
180
|
+
buildTrackedSnapshot(currentSnapshot, form, excludedFieldNames) !==
|
|
181
|
+
buildTrackedSnapshot(baselineSnapshot, form, excludedFieldNames);
|
|
136
182
|
|
|
137
183
|
return isDirty;
|
|
138
184
|
};
|
|
139
185
|
|
|
140
186
|
const captureBaseline = (): void => {
|
|
141
|
-
|
|
187
|
+
if (destroyed) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
clearScheduledWork();
|
|
191
|
+
baselineSnapshot = Array.from(new FormData(form).entries());
|
|
142
192
|
isReady = true;
|
|
143
193
|
isDirty = false;
|
|
144
194
|
};
|
|
145
195
|
|
|
146
196
|
const scheduleBaselineCapture = (): void => {
|
|
197
|
+
if (destroyed) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
147
200
|
clearScheduledWork();
|
|
148
201
|
isReady = false;
|
|
149
202
|
|
|
@@ -183,13 +236,16 @@ export function createFormUnloadWarningGuard(
|
|
|
183
236
|
form.addEventListener('input', scheduleDirtyRefresh);
|
|
184
237
|
form.addEventListener('change', scheduleDirtyRefresh);
|
|
185
238
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
186
|
-
|
|
239
|
+
captureBaseline();
|
|
187
240
|
|
|
188
241
|
return {
|
|
189
242
|
captureBaseline,
|
|
190
243
|
scheduleBaselineCapture,
|
|
191
244
|
refreshDirtyState,
|
|
192
245
|
destroy: () => {
|
|
246
|
+
destroyed = true;
|
|
247
|
+
isReady = false;
|
|
248
|
+
baselineSnapshot = [];
|
|
193
249
|
clearScheduledWork();
|
|
194
250
|
form.removeEventListener('input', scheduleDirtyRefresh);
|
|
195
251
|
form.removeEventListener('change', scheduleDirtyRefresh);
|
|
@@ -7,6 +7,24 @@ const required: ValidationRuleDefinition = {
|
|
|
7
7
|
return true;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Required tables need rows, not a value in every cell. Cell values are
|
|
11
|
+
// optional, including unchecked checkboxes and blank email addresses.
|
|
12
|
+
if (input.closest('[data-formie-field-handle]')?.getAttribute('data-formie-field-type') === 'table') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (input instanceof HTMLInputElement && input.type === 'file') {
|
|
17
|
+
if (input.files?.length) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const field = input.closest('[data-formie-field-handle]');
|
|
22
|
+
const name = input.name.replace(/\[\]$/, '');
|
|
23
|
+
|
|
24
|
+
return Array.from(field?.querySelectorAll<HTMLInputElement>('input[type="hidden"][data-formie-file-upload-asset-id]') || [])
|
|
25
|
+
.some((asset) => !asset.disabled && asset.name.startsWith(`${name}[`) && Number(asset.getAttribute('data-formie-file-upload-asset-id')) > 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
11
29
|
const checkboxInputs = input.form?.querySelectorAll(`[name="${input.name}"]:not([type="hidden"]):not([disabled])`) || [];
|
|
12
30
|
|
|
@@ -33,8 +33,10 @@ export function syncSubmitReadiness(form: HTMLFormElement, validator: FormieVali
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const { scope, final } = getValidationScope(form);
|
|
36
|
+
// Readiness observes DOM changes; redrawing errors here would retrigger it indefinitely.
|
|
36
37
|
const isReady = validator.isValid(scope, {
|
|
37
38
|
includeHiddenPages: final,
|
|
39
|
+
renderErrors: false,
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
form.setAttribute(SUBMIT_READY_ATTR, isReady ? 'true' : 'false');
|
|
@@ -25,6 +25,7 @@ type ValidatorDefinition = {
|
|
|
25
25
|
|
|
26
26
|
type ValidateOptions = {
|
|
27
27
|
includeHiddenPages?: boolean;
|
|
28
|
+
renderErrors?: boolean;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
const DEFAULT_PATTERNS: Record<string, RegExp> = {
|
|
@@ -40,11 +41,13 @@ const DEFAULT_PATTERNS: Record<string, RegExp> = {
|
|
|
40
41
|
const debug = createDebug('general', 'validator');
|
|
41
42
|
|
|
42
43
|
function isValidationInput(node: Element | null): node is ValidationInput {
|
|
44
|
+
// Enhancement libraries inject unnamed search controls that do not submit
|
|
45
|
+
// field values and must not inherit the field's validation rules.
|
|
43
46
|
return !!node && (
|
|
44
47
|
node instanceof HTMLInputElement ||
|
|
45
48
|
node instanceof HTMLSelectElement ||
|
|
46
49
|
node instanceof HTMLTextAreaElement
|
|
47
|
-
);
|
|
50
|
+
) && !!node.name;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function isPainted(element: HTMLElement): boolean {
|
|
@@ -161,6 +164,7 @@ export class FormieValidator {
|
|
|
161
164
|
|
|
162
165
|
this.inputs(inputOrSelector).forEach((input) => {
|
|
163
166
|
let errorShown = false;
|
|
167
|
+
const renderErrors = options.renderErrors !== false && this.shouldShowError(input);
|
|
164
168
|
|
|
165
169
|
if (!this.isVisible(input, options)) {
|
|
166
170
|
return;
|
|
@@ -179,7 +183,7 @@ export class FormieValidator {
|
|
|
179
183
|
seenGroups.add(groupKey);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
if (
|
|
186
|
+
if (renderErrors) {
|
|
183
187
|
this.removeError(input);
|
|
184
188
|
}
|
|
185
189
|
|
|
@@ -191,7 +195,7 @@ export class FormieValidator {
|
|
|
191
195
|
if (!isValid) {
|
|
192
196
|
const errorMessage = this.getErrorMessage(input, validatorName, validatorConfig, opts);
|
|
193
197
|
|
|
194
|
-
if (
|
|
198
|
+
if (renderErrors && !errorShown) {
|
|
195
199
|
this.showError(input, validatorName, errorMessage);
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -208,7 +212,7 @@ export class FormieValidator {
|
|
|
208
212
|
}
|
|
209
213
|
});
|
|
210
214
|
|
|
211
|
-
if (!errorShown &&
|
|
215
|
+
if (!errorShown && renderErrors) {
|
|
212
216
|
this.removeError(input);
|
|
213
217
|
}
|
|
214
218
|
});
|
|
@@ -298,8 +302,10 @@ export class FormieValidator {
|
|
|
298
302
|
errorMessages.innerHTML = '';
|
|
299
303
|
|
|
300
304
|
const handle = fieldContainer.getAttribute('data-formie-field-handle') || 'field';
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
errorMessages.id = errorMessages.id || `${input.id || `${this.form.id}-${handle}`}-errors`;
|
|
306
|
+
// Use the rendered field's scoped region so matching handles in other
|
|
307
|
+
// forms cannot capture this control's accessible error reference.
|
|
308
|
+
const errorId = `${errorMessages.id}-1`;
|
|
303
309
|
applyErrorAriaLive(
|
|
304
310
|
errorMessages,
|
|
305
311
|
resolveValidationErrorAriaLive(this.config.errorAriaLive, this.submitted),
|
|
@@ -485,6 +491,23 @@ export class FormieValidator {
|
|
|
485
491
|
return isPainted(richTextHost);
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
// Searchable selects keep their value in a hidden native control. Its
|
|
495
|
+
// adjacent combobox determines whether that value is active for validation.
|
|
496
|
+
if (element instanceof HTMLSelectElement && element.hasAttribute('data-formie-combobox-input')) {
|
|
497
|
+
const combobox = element.nextElementSibling;
|
|
498
|
+
|
|
499
|
+
if (combobox instanceof HTMLElement && combobox.classList.contains('formie-combobox')) {
|
|
500
|
+
return isPainted(combobox);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Third-party select enhancements hide the native value control without
|
|
505
|
+
// Formie's combobox marker. The containing field still determines whether
|
|
506
|
+
// its selected values need validation, including an empty required value.
|
|
507
|
+
if (element instanceof HTMLSelectElement && field instanceof HTMLElement) {
|
|
508
|
+
return isPainted(element) || isPainted(field);
|
|
509
|
+
}
|
|
510
|
+
|
|
488
511
|
return isPainted(element);
|
|
489
512
|
}
|
|
490
513
|
|
package/src/vendor.d.ts
CHANGED
|
@@ -58,7 +58,9 @@ declare module 'flatpickr/dist/l10n/index.js' {
|
|
|
58
58
|
|
|
59
59
|
declare module 'intl-tel-input' {
|
|
60
60
|
type IntlTelInputInstance = {
|
|
61
|
+
readonly promise: Promise<void>;
|
|
61
62
|
setCountry(country: string): void;
|
|
63
|
+
setNumber(number: string): void;
|
|
62
64
|
isValidNumber(): boolean;
|
|
63
65
|
getSelectedCountryData(): { iso2?: string } | null;
|
|
64
66
|
getValidationError(): number;
|