@verbb/formie-browser 1.0.15 → 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.
Files changed (135) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/chunks/{address-country-BcEQKw0T.js → address-country-DLJF-OkC.js} +4 -8
  3. package/dist/chunks/{address-finder-BWGINCSA.js → address-finder-QK1m1eyv.js} +1 -1
  4. package/dist/chunks/{address-state-DI3ndhMU.js → address-state-CHKit8AW.js} +26 -35
  5. package/dist/chunks/{api-BK9b340y.js → api-BPtH5RBA.js} +152 -120
  6. package/dist/chunks/{api-Dxl_CKw-.js → api-BR2Gqy37.js} +306 -301
  7. package/dist/chunks/{api-ghNr2uxX.js → api-CmuOZ0i3.js} +1 -1
  8. package/dist/chunks/{bpoint-BwtuyN_O.js → bpoint-GvYfyPjD.js} +1 -1
  9. package/dist/chunks/{calculations-YEoVCG2q.js → calculations-DeEmdUDq.js} +5 -7
  10. package/dist/chunks/{captcha-eu-DWLoQFKB.js → captcha-eu-eU_M6snp.js} +1 -1
  11. package/dist/chunks/{checkbox-radio-gsm8QPJg.js → checkbox-radio-CSXo6svm.js} +4 -12
  12. package/dist/chunks/{combobox-Ba08_cHL.js → combobox-CJh2DoSO.js} +292 -333
  13. package/dist/chunks/{conditions-Ceeg6ReZ.js → conditions-Dz3068K0.js} +56 -72
  14. package/dist/chunks/{country-from-ip-DDj_mFki.js → country-from-ip-BqQ-BqLH.js} +1 -1
  15. package/dist/chunks/{custom-google-maps-C4l2S3GO.js → custom-google-maps-rNm5VFbb.js} +2 -6
  16. package/dist/chunks/{custom-link-CLazgKYH.js → custom-link-CLqFCssQ.js} +1 -1
  17. package/dist/chunks/{custom-maps-DqcAlD0v.js → custom-maps-DgjYQDLj.js} +4 -8
  18. package/dist/chunks/{date-picker-DaPvMrop.js → date-picker-BVJxJVYT.js} +30 -47
  19. package/dist/chunks/{debug-BV0DvdHx.js → debug-rhzqP1j2.js} +2 -14
  20. package/dist/chunks/{dist-ZLeW0zZ3.js → dist-CJReDlDK.js} +13 -8
  21. package/dist/chunks/{eway-uw0DzU-L.js → eway-BZuTBAUW.js} +1 -1
  22. package/dist/chunks/{field-references.resolver-Ca8MguFv.js → field-references.resolver-uSHf5-NP.js} +1 -1
  23. package/dist/chunks/{file-upload-B8_mH012.js → file-upload-vEUr_EuP.js} +114 -108
  24. package/dist/chunks/{friendly-captcha-v1-CVXaXgDO.js → friendly-captcha-v1-C_cF1WD2.js} +2 -2
  25. package/dist/chunks/{friendly-captcha-v2-DkeIVTKI.js → friendly-captcha-v2-BenESH-n.js} +2 -2
  26. package/dist/chunks/{friendly-challenge-DPG9MJb1.js → friendly-challenge-BgPBEMXx.js} +21 -35
  27. package/dist/chunks/{go-cardless-c4wLJdOt.js → go-cardless-DWZeB0Bq.js} +2 -6
  28. package/dist/chunks/{google-address-JHYvjwgN.js → google-address-DQR6rsbh.js} +2 -2
  29. package/dist/chunks/{hcaptcha-Dt3ELLlP.js → hcaptcha-COilLmRT.js} +1 -1
  30. package/dist/chunks/{hidden-BZhXnNPe.js → hidden-Bc2TpcgF.js} +1 -1
  31. package/dist/chunks/{http-C2QdHuWO.js → http-Dj_skqaa.js} +1 -1
  32. package/dist/chunks/{leaflet-src-DE6rv6M-.js → leaflet-src-Cg-LUOD8.js} +74 -122
  33. package/dist/chunks/{loqate-m51iPyO1.js → loqate-ZZClcacr.js} +1 -1
  34. package/dist/chunks/{mollie-Bn0PX173.js → mollie-8OMXIYa8.js} +2 -6
  35. package/dist/chunks/{moneris-BRbUvI4t.js → moneris-qYHJ9EFy.js} +1 -1
  36. package/dist/chunks/opayo-DlWPEnla.js +159 -0
  37. package/dist/chunks/{paddle-CympHVvd.js → paddle-CArfWih0.js} +1 -1
  38. package/dist/chunks/{password-validation-C8_uO_c_.js → password-validation-HGOU9kf3.js} +2 -2
  39. package/dist/chunks/{paypal-h75h6wae.js → paypal-BcaMCMGY.js} +1 -1
  40. package/dist/chunks/{payway-C9rHTCC_.js → payway-vxC7_bfR.js} +3 -11
  41. package/dist/chunks/{phone-country-B8fv-L5_.js → phone-country-Bx9EG50h.js} +123 -140
  42. package/dist/chunks/{place-kit-BnZGQ-L1.js → place-kit-BKk8xDXf.js} +2 -2
  43. package/dist/chunks/{placekit-autocomplete.esm-CZZERYur.js → placekit-autocomplete.esm-D2A_0iVU.js} +2 -5
  44. package/dist/chunks/{recaptcha-enterprise-CUs8BO6V.js → recaptcha-enterprise-Cx6HUpMa.js} +6 -12
  45. package/dist/chunks/recaptcha-shared-CcMI-r8t.js +33 -0
  46. package/dist/chunks/{recaptcha-v2-checkbox-P1McW4Pv.js → recaptcha-v2-checkbox-DQ9BJzcv.js} +2 -2
  47. package/dist/chunks/{recaptcha-v2-invisible-FGDqozSP.js → recaptcha-v2-invisible-BP0T8Pan.js} +2 -2
  48. package/dist/chunks/recaptcha-v3-DKMGQ27K.js +38 -0
  49. package/dist/chunks/{repeater-CyhDbozO.js → repeater-BR0RUvTs.js} +3 -7
  50. package/dist/chunks/{rich-text-uV4o_pGk.js → rich-text-H2A6cm1t.js} +33 -11
  51. package/dist/chunks/{rolldown-runtime-DtPi1Y-2.js → rolldown-runtime-VHNaYcLd.js} +1 -1
  52. package/dist/chunks/{sdk-lkwSvlpM.js → sdk-_cmrRYgp.js} +4 -16
  53. package/dist/chunks/{shared-OKhSay59.js → shared-CJ1LIEh3.js} +1 -3
  54. package/dist/chunks/{signature-7cldFbqj.js → signature-CfKA_poI.js} +9 -41
  55. package/dist/chunks/{snaptcha-C3QLYxFT.js → snaptcha-P3EoSQud.js} +1 -1
  56. package/dist/chunks/{square-C1z5DPVw.js → square-Dyz0p_GU.js} +1 -1
  57. package/dist/chunks/{stripe-Dmkdkq1y.js → stripe-Bfd_jp_k.js} +3 -6
  58. package/dist/chunks/{summary-Bn-g5SDx.js → summary-B3CAebLV.js} +44 -36
  59. package/dist/chunks/{survey-likert-fk1hUPff.js → survey-likert-CEtUceag.js} +1 -1
  60. package/dist/chunks/{survey-rank-BsLGl2LX.js → survey-rank-Dtt97y6s.js} +3 -7
  61. package/dist/chunks/{survey-rating-DVS6e6fM.js → survey-rating-mlDMI0uq.js} +9 -23
  62. package/dist/chunks/{table-j7MlLLCc.js → table-0k6Md0IO.js} +1 -1
  63. package/dist/chunks/{text-limit-LGMPhFuC.js → text-limit-CTVML6s7.js} +10 -10
  64. package/dist/chunks/{turnstile-ptihQfLl.js → turnstile-Sp4ubowK.js} +1 -1
  65. package/dist/chunks/{upload-manager-BW7l-BhF.js → upload-manager-Ccg3d_4I.js} +11 -25
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +752 -756
  69. package/dist/js/core/create-formie-client.d.ts.map +1 -1
  70. package/dist/js/core/dom-events.d.ts +5 -0
  71. package/dist/js/core/dom-events.d.ts.map +1 -1
  72. package/dist/js/core/page-client-event.d.ts.map +1 -1
  73. package/dist/js/core/submit-result-state.d.ts.map +1 -1
  74. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  75. package/dist/js/core/validation-focus.d.ts.map +1 -1
  76. package/dist/js/modules/captchas/factories.d.ts.map +1 -1
  77. package/dist/js/modules/captchas/host.d.ts +4 -1
  78. package/dist/js/modules/captchas/host.d.ts.map +1 -1
  79. package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -1
  80. package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
  81. package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
  82. package/dist/js/modules/fields/address-state.d.ts.map +1 -1
  83. package/dist/js/modules/fields/combobox.d.ts +2 -0
  84. package/dist/js/modules/fields/combobox.d.ts.map +1 -1
  85. package/dist/js/modules/fields/conditions.d.ts.map +1 -1
  86. package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
  87. package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
  88. package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
  89. package/dist/js/modules/fields/summary.d.ts.map +1 -1
  90. package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
  91. package/dist/js/modules/payments/factories.d.ts.map +1 -1
  92. package/dist/js/modules/payments/opayo.d.ts.map +1 -1
  93. package/dist/js/submit/pipeline.d.ts +2 -0
  94. package/dist/js/submit/pipeline.d.ts.map +1 -1
  95. package/dist/js/utils/form-started-at.d.ts +1 -0
  96. package/dist/js/utils/form-started-at.d.ts.map +1 -1
  97. package/dist/js/utils/unload-warning.d.ts +4 -0
  98. package/dist/js/utils/unload-warning.d.ts.map +1 -1
  99. package/dist/js/validation/rules/required.d.ts.map +1 -1
  100. package/dist/js/validation/submit-readiness.d.ts.map +1 -1
  101. package/dist/js/validation/validator.d.ts +1 -0
  102. package/dist/js/validation/validator.d.ts.map +1 -1
  103. package/package.json +3 -3
  104. package/src/index.ts +1 -0
  105. package/src/js/core/create-formie-client.ts +20 -18
  106. package/src/js/core/dom-events.ts +30 -1
  107. package/src/js/core/page-client-event.ts +5 -7
  108. package/src/js/core/submit-result-state.ts +13 -2
  109. package/src/js/core/submit-result-ui.ts +21 -0
  110. package/src/js/core/validation-focus.ts +17 -3
  111. package/src/js/modules/captchas/factories.ts +241 -101
  112. package/src/js/modules/captchas/host.ts +16 -0
  113. package/src/js/modules/captchas/recaptcha-enterprise.ts +1 -1
  114. package/src/js/modules/captchas/recaptcha-shared.ts +23 -5
  115. package/src/js/modules/captchas/recaptcha-v3.ts +23 -14
  116. package/src/js/modules/fields/address-state.ts +6 -4
  117. package/src/js/modules/fields/combobox.ts +19 -1
  118. package/src/js/modules/fields/conditions.ts +3 -0
  119. package/src/js/modules/fields/file-upload.ts +11 -4
  120. package/src/js/modules/fields/phone-country.ts +23 -0
  121. package/src/js/modules/fields/rich-text.ts +33 -0
  122. package/src/js/modules/fields/summary.ts +63 -10
  123. package/src/js/modules/fields/text-limit.ts +11 -0
  124. package/src/js/modules/payments/factories.ts +87 -54
  125. package/src/js/modules/payments/opayo.ts +7 -0
  126. package/src/js/submit/pipeline.ts +16 -9
  127. package/src/js/utils/form-started-at.ts +19 -4
  128. package/src/js/utils/unload-warning.ts +63 -7
  129. package/src/js/validation/rules/required.ts +6 -0
  130. package/src/js/validation/submit-readiness.ts +2 -0
  131. package/src/js/validation/validator.ts +15 -4
  132. package/src/vendor.d.ts +2 -0
  133. package/dist/chunks/opayo-BwWdInEP.js +0 -188
  134. package/dist/chunks/recaptcha-shared-DTbTw-MF.js +0 -27
  135. package/dist/chunks/recaptcha-v3-CflUHeZD.js +0 -28
@@ -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 bus.emitSafe('formie:submit:after', abortedResult);
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 bus.emitSafe('formie:submit:final:after', abortedResult);
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 bus.emitSafe('formie:submit:after', abortedResult);
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 bus.emitSafe('formie:submit:final:after', abortedResult);
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 bus.emitSafe('formie:submit:after', stageResult);
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 bus.emitSafe('formie:submit:final:after', stageResult);
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 bus.emitSafe('formie:submit:after', successResult);
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 bus.emitSafe('formie:submit:final:after', successResult);
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,
@@ -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
- let startedAt = storageKey ? sessionStorage.getItem(storageKey) : null;
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
- if (storageKey) {
20
- sessionStorage.setItem(storageKey, startedAt);
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(form: HTMLFormElement): string {
86
- const entries = Array.from(new FormData(form).entries()).filter(([name]) => {
87
- return shouldTrackFieldName(String(name || ''), form);
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: string | null = null;
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
- isDirty = buildTrackedSnapshot(form) !== baselineSnapshot;
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
- baselineSnapshot = buildTrackedSnapshot(form);
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
- scheduleBaselineCapture();
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,12 @@ 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
+
10
16
  if (input instanceof HTMLInputElement && input.type === 'file') {
11
17
  if (input.files?.length) {
12
18
  return true;
@@ -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 (this.shouldShowError(input)) {
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 (this.shouldShowError(input) && !errorShown) {
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 && this.shouldShowError(input)) {
215
+ if (!errorShown && renderErrors) {
212
216
  this.removeError(input);
213
217
  }
214
218
  });
@@ -497,6 +501,13 @@ export class FormieValidator {
497
501
  }
498
502
  }
499
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
+
500
511
  return isPainted(element);
501
512
  }
502
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;
@@ -1,188 +0,0 @@
1
- import { t as e } from "./request-profile-DhwkeCpS.js";
2
- import { t } from "./dist-ZLeW0zZ3.js";
3
- import { s as n } from "./event-names-BCI2FLD8.js";
4
- import { t as r } from "./api-Dxl_CKw-.js";
5
- import { t as i } from "./debug-BV0DvdHx.js";
6
- import { a } from "./theme-classes-DAQuEqdP.js";
7
- import { t as o } from "./csrf-DxHg_ZYt.js";
8
- import { r as s } from "./scripts-DrccjrFv.js";
9
- import { t as c } from "./styles-BfoIZwJp.js";
10
- //#endregion
11
- //#region src/js/modules/payments/opayo.ts
12
- c("opayo", ["@layer formie-theme{.formie-opayo-drop-in{box-sizing:border-box;width:100%;min-height:10rem}}"]);
13
- var l = "FORMIE_OPAYO_SCRIPT", u = "https://live.opayo.eu.elavon.com/api/v1/js/sagepay.js", d = "https://sandbox.opayo.eu.elavon.com/api/v1/js/sagepay.js", f = "[data-formie-opayo-drop-in]", p = i("payments", "opayo"), m = n("opayo", "challenge"), h = "formie:payment:opayo:challenge:response";
14
- function g(e) {
15
- return e.checkoutMode === "dropIn";
16
- }
17
- async function _(n) {
18
- let { form: r, handle: i, sessionToken: a, services: s } = n, c = new FormData();
19
- o(c, r), c.set("action", "formie/payment-sessions/initialize"), c.append("handle", i), c.append("sessionToken", a);
20
- try {
21
- let i = await t(n.sessionEndpoint || r.action, {
22
- method: "POST",
23
- body: c
24
- }, e(r));
25
- return i.status < 200 || i.status >= 300 ? (s.addError(`${i.status}: ${i.statusText}`), p.warn("Merchant session request failed.", {
26
- status: i.status,
27
- statusText: i.statusText
28
- }), null) : (await i.json()).merchantSessionKey || (s.addError("Unable to get merchant session."), p.warn("merchantSessionKey missing in session response."), null);
29
- } catch {
30
- return s.addError("Network error. Please try again."), p.warn("Network error requesting merchant session."), null;
31
- }
32
- }
33
- function v(e) {
34
- return e.id ||= `formie-opayo-drop-in-${Math.random().toString(36).slice(2, 9)}`, e.id;
35
- }
36
- var y = r({
37
- moduleId: "formie:opayo",
38
- defaultRequiredInputSuffixes: ["opayoTokenId"],
39
- load: async (e) => {
40
- let { provider: t } = e.options, n = t.useSandbox ? d : u, r = g(t) ? "sagepayCheckout" : "sagepayOwnForm";
41
- return await s(r, {
42
- id: l,
43
- src: n,
44
- timeoutMs: 1e4
45
- }), null;
46
- },
47
- mount: async ({ field: e, services: t, provider: n }) => {
48
- if (!g(n)) return null;
49
- let r = t.form, i = window.sagepayCheckout, a = e.querySelector(f);
50
- if (!r?.action) return t.addError("Form action is missing."), p.warn("Missing form action before drop-in mount."), null;
51
- if (!i) return t.addError("Opayo script failed to load."), p.warn("sagepayCheckout global not available."), null;
52
- if (!a) return t.addError("Opayo drop-in container is missing."), p.warn("Drop-in container not found in payment field."), null;
53
- let o = n.handle || "opayo", s = await _({
54
- form: r,
55
- handle: o,
56
- sessionToken: n.sessionToken || "",
57
- sessionEndpoint: n.sessionEndpoint,
58
- services: t
59
- });
60
- if (!s) return null;
61
- let c = v(a), l = {
62
- checkout: null,
63
- merchantSessionKey: s,
64
- pendingAuthorize: null,
65
- retriedTokenise: !1
66
- };
67
- return l.checkout = i({
68
- merchantSessionKey: s,
69
- containerSelector: `#${c}`,
70
- onTokenise: (e) => {
71
- let i = l.pendingAuthorize;
72
- if (l.pendingAuthorize = null, !i) {
73
- p.warn("Drop-in tokenisation completed without a pending authorize step.");
74
- return;
75
- }
76
- if (e.success && e.cardIdentifier) {
77
- t.updateInputs("opayoTokenId", e.cardIdentifier), t.updateInputs("opayoSessionKey", l.merchantSessionKey), p.log("Drop-in tokenization succeeded.", { hasCardIdentifier: !!e.cardIdentifier }), i(!0);
78
- return;
79
- }
80
- if (!l.retriedTokenise) {
81
- l.retriedTokenise = !0, _({
82
- form: r,
83
- handle: o,
84
- sessionToken: n.sessionToken || "",
85
- sessionEndpoint: n.sessionEndpoint,
86
- services: t
87
- }).then((n) => {
88
- if (!n) {
89
- t.addError(e.errors?.[0]?.message || "Tokenization failed."), p.warn("Drop-in tokenization failed after session refresh.", e), i(!1);
90
- return;
91
- }
92
- l.merchantSessionKey = n, l.pendingAuthorize = i, l.checkout.tokenise({ newMerchantSessionKey: n });
93
- });
94
- return;
95
- }
96
- t.addError(e.errors?.[0]?.message || "Tokenization failed."), p.warn("Drop-in tokenization failed.", e), i(!1);
97
- }
98
- }), p.log("Drop-in checkout mounted.", { containerId: c }), l;
99
- },
100
- unmount: async ({ widget: e }) => {
101
- e?.checkout?.destroy?.();
102
- },
103
- onBeforePayment: async (e) => {
104
- let { field: t, services: n, options: r, provider: i, widget: a } = e, o = i.handle || "opayo", s = n.form;
105
- if (!s?.action) return n.addError("Form action is missing."), p.warn("Missing form action before authorize."), !1;
106
- if (g(i)) return a ? (a.retriedTokenise = !1, new Promise((e) => {
107
- a.pendingAuthorize = e, a.checkout.tokenise();
108
- })) : (n.addError("Opayo drop-in checkout is not ready."), p.warn("Drop-in authorize requested before widget mount."), !1);
109
- let c = window.sagepayOwnForm;
110
- if (!c) return n.addError("Opayo script failed to load."), p.warn("sagepayOwnForm global not available."), !1;
111
- let l = t.querySelector("[data-opayo-card=\"cardholder-name\"]")?.value ?? "", u = t.querySelector("[data-opayo-card=\"card-number\"]")?.value ?? "", d = t.querySelector("[data-opayo-card=\"expiry-date\"]")?.value ?? "", f = t.querySelector("[data-opayo-card=\"security-code\"]")?.value ?? "";
112
- u = u.replace(/[\s/]/g, ""), d = d.replace(/[\s/]/g, "");
113
- let m = await _({
114
- form: s,
115
- handle: o,
116
- sessionToken: i.sessionToken || "",
117
- sessionEndpoint: i.sessionEndpoint,
118
- services: n
119
- });
120
- return m ? new Promise((e) => {
121
- c({ merchantSessionKey: m }).tokeniseCardDetails({
122
- cardDetails: {
123
- cardholderName: l,
124
- cardNumber: u,
125
- expiryDate: d,
126
- securityCode: f
127
- },
128
- onTokenised: (t) => {
129
- t.success && t.cardIdentifier ? (n.updateInputs("opayoTokenId", t.cardIdentifier), n.updateInputs("opayoSessionKey", m), p.log("Tokenization succeeded.", { hasCardIdentifier: !!t.cardIdentifier }), e(!0)) : (n.addError(t.errors?.[0]?.message || "Tokenization failed."), p.warn("Tokenization failed.", t), e(!1));
130
- }
131
- });
132
- }) : !1;
133
- },
134
- setup: async (e) => {
135
- let { services: t } = e;
136
- e.target;
137
- let n = null, r = !1, i = () => {
138
- n?.parentNode && n.parentNode.removeChild(n), n = null;
139
- }, o = t.events.onForm(m, ((e) => {
140
- let o = e.detail?.data;
141
- if (!o?.acsUrl || !o?.creq) return;
142
- r = !1, p.log("Received payment challenge event.", {
143
- hasAcsUrl: !!o.acsUrl,
144
- hasCreq: !!o.creq
145
- });
146
- let s = t.form, c = s?.querySelector("input[name*=\"opayoSessionKey\"]")?.value || "", l = document.createElement("div");
147
- l.className = "formie-modal", l.id = `formie-opayo-dialog-${Math.random().toString(36).slice(2, 9)}`, l.innerHTML = "\n <div class=\"formie-modal-backdrop\" data-dialog-close></div>\n <div class=\"formie-modal-content\">\n <div data-formie-opayo-loading style=\"--formie-loading-width: 3rem; --formie-loading-height: 3rem; top: 50%; margin-top: -1.5rem;\"></div>\n <iframe width=\"100%\" height=\"100%\" style=\"width: 100%; height: 100%; position: relative; z-index: 1;\"></iframe>\n </div>\n ";
148
- let u = l.querySelector("[data-formie-opayo-loading]");
149
- u && a(u, s, "loading", !0);
150
- let d = l.querySelector("iframe"), f = (e) => e.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;"), m = o.returnUrl || o.redirectUrl || "", h = `<form action="${f(o.acsUrl)}" method="post">
151
- <input type="hidden" name="creq" value="${f(o.creq || "")}" />
152
- <input type="hidden" name="threeDSSessionData" value="${f(o.threeDSSessionData || "")}" />
153
- <input type="hidden" name="MD" value="${f(c)}" />
154
- <input type="hidden" name="TermUrl" value="${f(m)}" />
155
- <input type="hidden" name="ThreeDSNotificationURL" value="${f(m)}" />
156
- </form><script>document.forms[0].submit();<\/script>`;
157
- i(), document.body.appendChild(l), n = l, d?.contentWindow && (d.contentWindow.document.open(), d.contentWindow.document.write(h), d.contentWindow.document.close());
158
- })), s = (e) => {
159
- if (e.data?.message === h) {
160
- if (!n) {
161
- p.log("Ignoring 3DS response without active dialog.");
162
- return;
163
- }
164
- if (r) {
165
- p.warn("Ignoring duplicate 3DS response while processing.");
166
- return;
167
- }
168
- if (r = !0, p.log("Received payment challenge response message.", e.data?.value), i(), t.removeError(), e.data?.value?.error) {
169
- t.addError(e.data.value.error.message), t.releaseSubmitLoading(), r = !1;
170
- return;
171
- }
172
- t.updateInputs("opayo3DSComplete", e.data.value?.transactionId ?? ""), t.triggerSubmit();
173
- }
174
- };
175
- return window.addEventListener("message", s), { destroy: () => {
176
- o(), window.removeEventListener("message", s), i(), r = !1;
177
- } };
178
- },
179
- onAfterSubmit: async ({ services: e }) => {
180
- e.updateInputs([
181
- "opayoTokenId",
182
- "opayoSessionKey",
183
- "opayo3DSComplete"
184
- ], "");
185
- }
186
- });
187
- //#endregion
188
- export { y as opayoModule };
@@ -1,27 +0,0 @@
1
- import { a as e, r as t } from "./api-BK9b340y.js";
2
- import { r as n } from "./scripts-DrccjrFv.js";
3
- //#region src/js/modules/captchas/recaptcha-shared.ts
4
- async function r(r, i = !1, a) {
5
- let o = typeof r.language == "string" && r.language.trim() !== "" ? r.language.trim() : "en", { async: s, defer: c } = t(r.loadingMethod), l = i ? "https://www.google.com/recaptcha/enterprise.js" : "https://www.recaptcha.net/recaptcha/api.js", u = typeof a == "string" && a.trim() !== "" ? a.trim() : "explicit", d = new URL(l);
6
- return d.searchParams.set("render", u), d.searchParams.set("hl", o), u !== "explicit" && typeof r.badge == "string" && r.badge.trim() !== "" && d.searchParams.set("badge", r.badge.trim()), n("grecaptcha", {
7
- id: i ? "FORMIE_RECAPTCHA_ENTERPRISE_SCRIPT" : "FORMIE_RECAPTCHA_SCRIPT",
8
- src: d.toString(),
9
- async: s,
10
- defer: c,
11
- timeoutMs: e
12
- });
13
- }
14
- function i(e, t) {
15
- let n = e.enterprise || e;
16
- return new Promise((e, r) => {
17
- try {
18
- n.ready(() => {
19
- Promise.resolve(t()).then(e).catch(r);
20
- });
21
- } catch (e) {
22
- r(e);
23
- }
24
- });
25
- }
26
- //#endregion
27
- export { i as n, r as t };
@@ -1,28 +0,0 @@
1
- import { t as e } from "./api-BK9b340y.js";
2
- import { n as t, t as n } from "./recaptcha-shared-DTbTw-MF.js";
3
- //#region src/js/modules/captchas/recaptcha-v3.ts
4
- var r = e({
5
- moduleId: "formie:recaptcha-v3",
6
- defaultPlaceholderSelector: "[data-recaptcha-placeholder]",
7
- defaultTokenFieldNames: ["g-recaptcha-response"],
8
- load: ({ options: e }) => n(e.provider, !1, e.provider.siteKey || void 0),
9
- mount: ({ api: e, provider: t }) => new Promise((n) => {
10
- e.ready(() => {
11
- n(t.siteKey || "recaptcha-v3");
12
- });
13
- }),
14
- challenge: async ({ api: e, provider: n, placeholder: r, services: i, stageCtx: a }) => {
15
- if (!i.tokens.has() && (await t(e, async () => {
16
- let t = await e.execute(n.siteKey || "", { action: n.action || "submit" });
17
- typeof t == "string" && t.trim() !== "" && i.tokens.write(t.trim());
18
- }), !await i.tokens.wait(12e4))) {
19
- let e = i.errors.getDefaultMessage();
20
- i.errors.show(e, r), a.abort(e);
21
- }
22
- },
23
- unmount: ({ services: e }) => {
24
- e.tokens.clear();
25
- }
26
- });
27
- //#endregion
28
- export { r as recaptchaV3Module };