@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/chunks/{address-country-Dn71jroa.js → address-country-DLJF-OkC.js} +5 -9
  3. package/dist/chunks/{address-finder-O49uDRoR.js → address-finder-QK1m1eyv.js} +2 -2
  4. package/dist/chunks/{address-state-ByYatUur.js → address-state-CHKit8AW.js} +113 -112
  5. package/dist/chunks/{api-DYYsVIub.js → api-BPtH5RBA.js} +162 -130
  6. package/dist/chunks/{api-D6kJ7kXZ.js → api-BR2Gqy37.js} +313 -303
  7. package/dist/chunks/{api-ghNr2uxX.js → api-CmuOZ0i3.js} +1 -1
  8. package/dist/chunks/{async-nPFRNQ06.js → async-2GdXNFCE.js} +18 -1
  9. package/dist/chunks/{bpoint-D0RKFM4F.js → bpoint-GvYfyPjD.js} +1 -1
  10. package/dist/chunks/{calculations-YEoVCG2q.js → calculations-DeEmdUDq.js} +5 -7
  11. package/dist/chunks/{captcha-eu-DERJbXnH.js → captcha-eu-eU_M6snp.js} +2 -2
  12. package/dist/chunks/{checkbox-radio-gsm8QPJg.js → checkbox-radio-CSXo6svm.js} +4 -12
  13. package/dist/chunks/{combobox-CWNdx4Fk.js → combobox-CJh2DoSO.js} +323 -363
  14. package/dist/chunks/{conditions-DqNHLTbN.js → conditions-Dz3068K0.js} +140 -153
  15. package/dist/chunks/{country-from-ip-DDj_mFki.js → country-from-ip-BqQ-BqLH.js} +1 -1
  16. package/dist/chunks/{custom-google-maps-C4l2S3GO.js → custom-google-maps-rNm5VFbb.js} +2 -6
  17. package/dist/chunks/{custom-link-CLazgKYH.js → custom-link-CLqFCssQ.js} +1 -1
  18. package/dist/chunks/{custom-maps-DqcAlD0v.js → custom-maps-DgjYQDLj.js} +4 -8
  19. package/dist/chunks/{date-picker-RUnevF7l.js → date-picker-BVJxJVYT.js} +42 -58
  20. package/dist/chunks/{debug-BV0DvdHx.js → debug-rhzqP1j2.js} +2 -14
  21. package/dist/chunks/{dist-ZLeW0zZ3.js → dist-CJReDlDK.js} +13 -8
  22. package/dist/chunks/{eway-or6dtjoT.js → eway-BZuTBAUW.js} +3 -3
  23. package/dist/chunks/{field-references.resolver-Ca8MguFv.js → field-references.resolver-uSHf5-NP.js} +1 -1
  24. package/dist/chunks/{file-upload-Bm-TXyNB.js → file-upload-vEUr_EuP.js} +131 -113
  25. package/dist/chunks/{friendly-captcha-v1-GFqCt1Mh.js → friendly-captcha-v1-C_cF1WD2.js} +2 -2
  26. package/dist/chunks/{friendly-captcha-v2-WoW246JG.js → friendly-captcha-v2-BenESH-n.js} +2 -2
  27. package/dist/chunks/{friendly-challenge-DPG9MJb1.js → friendly-challenge-BgPBEMXx.js} +21 -35
  28. package/dist/chunks/{go-cardless-DzJ-0FKg.js → go-cardless-DWZeB0Bq.js} +2 -6
  29. package/dist/chunks/{google-address-C1P1nTDM.js → google-address-DQR6rsbh.js} +15 -10
  30. package/dist/chunks/{hcaptcha-D0T8aIHz.js → hcaptcha-COilLmRT.js} +2 -2
  31. package/dist/chunks/{hidden-BZhXnNPe.js → hidden-Bc2TpcgF.js} +1 -1
  32. package/dist/chunks/{http-C2QdHuWO.js → http-Dj_skqaa.js} +1 -1
  33. package/dist/chunks/{leaflet-src-DE6rv6M-.js → leaflet-src-Cg-LUOD8.js} +74 -122
  34. package/dist/chunks/{loqate-DD1S7PY9.js → loqate-ZZClcacr.js} +2 -2
  35. package/dist/chunks/{mollie-DG8Ju83x.js → mollie-8OMXIYa8.js} +2 -6
  36. package/dist/chunks/{moneris-C8m3Fvqu.js → moneris-qYHJ9EFy.js} +1 -1
  37. package/dist/chunks/opayo-DlWPEnla.js +159 -0
  38. package/dist/chunks/{paddle-BIYR9Um3.js → paddle-CArfWih0.js} +2 -2
  39. package/dist/chunks/{password-validation-C8_uO_c_.js → password-validation-HGOU9kf3.js} +2 -2
  40. package/dist/chunks/{paypal-B_yxIo2d.js → paypal-BcaMCMGY.js} +2 -2
  41. package/dist/chunks/{payway-CeOzS0sL.js → payway-vxC7_bfR.js} +4 -12
  42. package/dist/chunks/{phone-country-bAqje17P.js → phone-country-Bx9EG50h.js} +124 -141
  43. package/dist/chunks/{place-kit-BE5gi3vo.js → place-kit-BKk8xDXf.js} +10 -10
  44. package/dist/chunks/{placekit-autocomplete.esm-CZZERYur.js → placekit-autocomplete.esm-D2A_0iVU.js} +2 -5
  45. package/dist/chunks/{recaptcha-enterprise-CruXddZq.js → recaptcha-enterprise-Cx6HUpMa.js} +6 -12
  46. package/dist/chunks/recaptcha-shared-CcMI-r8t.js +33 -0
  47. package/dist/chunks/{recaptcha-v2-checkbox-CCnJYCnG.js → recaptcha-v2-checkbox-DQ9BJzcv.js} +2 -2
  48. package/dist/chunks/{recaptcha-v2-invisible-BuVqRRYn.js → recaptcha-v2-invisible-BP0T8Pan.js} +2 -2
  49. package/dist/chunks/recaptcha-v3-DKMGQ27K.js +38 -0
  50. package/dist/chunks/{repeater-DwE27jI1.js → repeater-BR0RUvTs.js} +50 -40
  51. package/dist/chunks/{rich-text-uV4o_pGk.js → rich-text-H2A6cm1t.js} +33 -11
  52. package/dist/chunks/{rolldown-runtime-DtPi1Y-2.js → rolldown-runtime-VHNaYcLd.js} +1 -1
  53. package/dist/chunks/{scripts-CbQ7agX3.js → scripts-DrccjrFv.js} +1 -1
  54. package/dist/chunks/{sdk-lkwSvlpM.js → sdk-_cmrRYgp.js} +4 -16
  55. package/dist/chunks/{shared-OKhSay59.js → shared-CJ1LIEh3.js} +1 -3
  56. package/dist/chunks/{signature-7cldFbqj.js → signature-CfKA_poI.js} +9 -41
  57. package/dist/chunks/skip-CEW89ExY.js +7 -0
  58. package/dist/chunks/{snaptcha-C04kNGYQ.js → snaptcha-P3EoSQud.js} +1 -1
  59. package/dist/chunks/{square-DEzxv9D9.js → square-Dyz0p_GU.js} +2 -2
  60. package/dist/chunks/{stripe-CVPQGDz3.js → stripe-Bfd_jp_k.js} +4 -7
  61. package/dist/chunks/{summary-BF_8mrXI.js → summary-B3CAebLV.js} +45 -37
  62. package/dist/chunks/{survey-likert-fk1hUPff.js → survey-likert-CEtUceag.js} +1 -1
  63. package/dist/chunks/{survey-rank-BsLGl2LX.js → survey-rank-Dtt97y6s.js} +3 -7
  64. package/dist/chunks/{survey-rating-DVS6e6fM.js → survey-rating-mlDMI0uq.js} +9 -23
  65. package/dist/chunks/{table-DnIBdeKF.js → table-0k6Md0IO.js} +2 -2
  66. package/dist/chunks/{text-limit-LGMPhFuC.js → text-limit-CTVML6s7.js} +10 -10
  67. package/dist/chunks/{turnstile-n9DNyPxi.js → turnstile-Sp4ubowK.js} +2 -2
  68. package/dist/chunks/{upload-manager-BbiUosZq.js → upload-manager-Ccg3d_4I.js} +258 -260
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +743 -734
  72. package/dist/js/core/create-formie-client.d.ts.map +1 -1
  73. package/dist/js/core/dom-events.d.ts +5 -0
  74. package/dist/js/core/dom-events.d.ts.map +1 -1
  75. package/dist/js/core/page-client-event.d.ts.map +1 -1
  76. package/dist/js/core/submit-result-state.d.ts.map +1 -1
  77. package/dist/js/core/submit-result-ui.d.ts +3 -1
  78. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  79. package/dist/js/core/validation-focus.d.ts.map +1 -1
  80. package/dist/js/modules/address/google-address.d.ts +10 -0
  81. package/dist/js/modules/address/google-address.d.ts.map +1 -1
  82. package/dist/js/modules/address/place-kit.d.ts.map +1 -1
  83. package/dist/js/modules/captchas/factories.d.ts.map +1 -1
  84. package/dist/js/modules/captchas/host.d.ts +4 -1
  85. package/dist/js/modules/captchas/host.d.ts.map +1 -1
  86. package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -1
  87. package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
  88. package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
  89. package/dist/js/modules/fields/address-state.d.ts.map +1 -1
  90. package/dist/js/modules/fields/combobox.d.ts +2 -0
  91. package/dist/js/modules/fields/combobox.d.ts.map +1 -1
  92. package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
  93. package/dist/js/modules/fields/conditions.d.ts.map +1 -1
  94. package/dist/js/modules/fields/date-picker.d.ts.map +1 -1
  95. package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
  96. package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
  97. package/dist/js/modules/fields/repeater.d.ts.map +1 -1
  98. package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
  99. package/dist/js/modules/fields/summary.d.ts.map +1 -1
  100. package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
  101. package/dist/js/modules/fields/upload-manager.d.ts.map +1 -1
  102. package/dist/js/modules/payments/factories.d.ts.map +1 -1
  103. package/dist/js/modules/payments/opayo.d.ts.map +1 -1
  104. package/dist/js/submit/pipeline.d.ts +2 -0
  105. package/dist/js/submit/pipeline.d.ts.map +1 -1
  106. package/dist/js/utils/async.d.ts +1 -0
  107. package/dist/js/utils/async.d.ts.map +1 -1
  108. package/dist/js/utils/form-started-at.d.ts +1 -0
  109. package/dist/js/utils/form-started-at.d.ts.map +1 -1
  110. package/dist/js/utils/unload-warning.d.ts +4 -0
  111. package/dist/js/utils/unload-warning.d.ts.map +1 -1
  112. package/dist/js/validation/rules/required.d.ts.map +1 -1
  113. package/dist/js/validation/submit-readiness.d.ts.map +1 -1
  114. package/dist/js/validation/validator.d.ts +1 -0
  115. package/dist/js/validation/validator.d.ts.map +1 -1
  116. package/package.json +3 -3
  117. package/src/index.ts +1 -0
  118. package/src/js/core/create-formie-client.ts +37 -28
  119. package/src/js/core/dom-events.ts +30 -1
  120. package/src/js/core/page-client-event.ts +5 -7
  121. package/src/js/core/submit-result-state.ts +30 -2
  122. package/src/js/core/submit-result-ui.ts +25 -3
  123. package/src/js/core/validation-focus.ts +17 -3
  124. package/src/js/modules/address/google-address.ts +95 -37
  125. package/src/js/modules/address/place-kit.ts +13 -7
  126. package/src/js/modules/captchas/factories.ts +241 -101
  127. package/src/js/modules/captchas/host.ts +16 -0
  128. package/src/js/modules/captchas/recaptcha-enterprise.ts +1 -1
  129. package/src/js/modules/captchas/recaptcha-shared.ts +23 -5
  130. package/src/js/modules/captchas/recaptcha-v3.ts +23 -14
  131. package/src/js/modules/fields/address-country.ts +5 -5
  132. package/src/js/modules/fields/address-state.ts +80 -27
  133. package/src/js/modules/fields/combobox.ts +22 -1
  134. package/src/js/modules/fields/conditions/effects.ts +4 -5
  135. package/src/js/modules/fields/conditions.ts +9 -0
  136. package/src/js/modules/fields/date-picker.ts +6 -3
  137. package/src/js/modules/fields/file-upload.ts +55 -18
  138. package/src/js/modules/fields/phone-country.ts +25 -2
  139. package/src/js/modules/fields/repeater.ts +55 -4
  140. package/src/js/modules/fields/rich-text.ts +33 -0
  141. package/src/js/modules/fields/summary.ts +63 -10
  142. package/src/js/modules/fields/text-limit.ts +11 -0
  143. package/src/js/modules/fields/upload-manager.ts +54 -18
  144. package/src/js/modules/payments/factories.ts +87 -54
  145. package/src/js/modules/payments/opayo.ts +7 -0
  146. package/src/js/submit/pipeline.ts +16 -9
  147. package/src/js/utils/async.ts +34 -0
  148. package/src/js/utils/form-started-at.ts +19 -4
  149. package/src/js/utils/unload-warning.ts +63 -7
  150. package/src/js/validation/rules/required.ts +18 -0
  151. package/src/js/validation/submit-readiness.ts +2 -0
  152. package/src/js/validation/validator.ts +29 -6
  153. package/src/vendor.d.ts +2 -0
  154. package/dist/chunks/opayo-B3EvrVxw.js +0 -188
  155. package/dist/chunks/recaptcha-shared-AQRMDMme.js +0 -27
  156. package/dist/chunks/recaptcha-v3-CloqhO2A.js +0 -28
@@ -1,6 +1,35 @@
1
- import { toDomEventName } from '#utils/event-names';
1
+ import type { FormieFormInstance } from '#contracts/client';
2
+ import { FORMIE_HTML_EVENT_NAMES, toDomEventName } from '#utils/event-names';
3
+
4
+ const formEventInstances = new WeakMap<Element, FormieFormInstance>();
5
+ const lifecycleEvents = new Set<string>([...FORMIE_HTML_EVENT_NAMES, 'formie:state:reset']);
6
+
7
+ export function bindFormEventInstance(root: Element, form: HTMLFormElement | null, instance: FormieFormInstance): () => void {
8
+ // Wrapper mounts also dispatch lifecycle events from their inner form.
9
+ const targets = new Set<Element>(form ? [root, form] : [root]);
10
+ targets.forEach((target) => formEventInstances.set(target, instance));
11
+ return () => {
12
+ targets.forEach((target) => {
13
+ if (formEventInstances.get(target) === instance) formEventInstances.delete(target);
14
+ });
15
+ };
16
+ }
17
+
18
+ export function withFormEventInstance<T extends object>(target: Element, detail: T): T & { instance?: FormieFormInstance } {
19
+ const instance = formEventInstances.get(target);
20
+ // Retain payload identity: synchronous hooks can modify the active result.
21
+ return instance ? Object.assign(detail, { instance }) : detail;
22
+ }
2
23
 
3
24
  export function dispatchFormieDomEvent(target: Element, eventName: string, detail: unknown): void {
25
+ // Early setup and standalone module hydration have no mounted form instance.
26
+ // Custom module events retain their own payload shapes.
27
+ if (lifecycleEvents.has(eventName) || eventName.startsWith('formie:stage:')) {
28
+ if (detail == null) detail = formEventInstances.has(target) ? {} : detail;
29
+ if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
30
+ detail = withFormEventInstance(target, detail);
31
+ }
32
+ }
4
33
  target.dispatchEvent(new CustomEvent(toDomEventName(eventName), {
5
34
  bubbles: true,
6
35
  detail,
@@ -1,3 +1,4 @@
1
+ import { dispatchFormieDomEvent } from '#core/dom-events';
1
2
  import { createDebug } from '#utils/debug';
2
3
 
3
4
  const debug = createDebug('general', 'page-client-event');
@@ -114,13 +115,10 @@ export function dispatchResolvedClientEvents(form: HTMLFormElement, events: Reso
114
115
  events.forEach((item) => {
115
116
  win.dataLayer!.push(item.payload);
116
117
 
117
- form.dispatchEvent(new CustomEvent('formie:client-event', {
118
- bubbles: true,
119
- detail: {
120
- event: item.event,
121
- payload: item.payload,
122
- },
123
- }));
118
+ dispatchFormieDomEvent(form, 'formie:client-event', {
119
+ event: item.event,
120
+ payload: item.payload,
121
+ });
124
122
  });
125
123
 
126
124
  debug.log('Dispatched resolved client events.', {
@@ -5,6 +5,7 @@ import { dispatchPageClientEventForSubmit, dispatchResolvedClientEvents } from '
5
5
  import { syncPageTabErrors } from '#core/page-tab-errors';
6
6
  import { addThemeClasses, removeThemeClasses, toggleThemeClasses } from '#theme/theme-classes';
7
7
  import { createDebug } from '#utils/debug';
8
+ import { resetFormStartedAt } from '#utils/form-started-at';
8
9
  import { getFormStateEventName, normalizeFormieEventName } from '#utils/event-names';
9
10
  import type { FormieValidator } from '#validation/validator';
10
11
  import { syncSubmitReadiness } from '#validation/submit-readiness';
@@ -371,6 +372,17 @@ export function applyPageState(form: HTMLFormElement, nextPageId: string): void
371
372
  }
372
373
 
373
374
  function syncSubmissionIdentity(form: HTMLFormElement, result: FormSubmitResult): void {
375
+ if (result.meta?.resetSubmissionIdentity === true) {
376
+ // Saved spam is terminal. Preserve the entered values for correction,
377
+ // but do not try to continue the rejected record on the next submit.
378
+ removeHiddenInput(form, 'submissionId');
379
+ removeHiddenInput(form, 'submissionUid');
380
+ removeHiddenInput(form, 'progressId');
381
+ setHiddenInputValue(form, 'expectedVersion', '0');
382
+ clearResumeTokenState(form);
383
+ return;
384
+ }
385
+
374
386
  const session = result.meta?.session as { version?: number } | undefined;
375
387
  const version = session?.version ?? result.meta?.version;
376
388
  if (typeof version === 'number') {
@@ -449,6 +461,15 @@ function resetSubmissionState(form: HTMLFormElement, options: ResetSubmissionSta
449
461
  // A full state reset is broader than the native form reset: it also clears
450
462
  // hidden continuity inputs, resume-token state, page UI, and live validation.
451
463
  form.reset();
464
+ // Summary modules on hidden pages are unmounted and cannot clear themselves.
465
+ form.querySelectorAll('[data-formie-summary-token]').forEach((element) => {
466
+ if (element instanceof HTMLInputElement) {
467
+ element.remove();
468
+ } else {
469
+ element.removeAttribute('data-formie-summary-token');
470
+ }
471
+ });
472
+ form.querySelectorAll('[data-formie-summary-blocks]').forEach((element) => element.replaceChildren());
452
473
  if (!options.preserveHiddenState) {
453
474
  setFormHiddenState(form, false);
454
475
  }
@@ -458,16 +479,17 @@ function resetSubmissionState(form: HTMLFormElement, options: ResetSubmissionSta
458
479
  removeHiddenInput(form, 'progressId');
459
480
  removeHiddenInput(form, 'pageId');
460
481
  clearResumeTokenState(form);
482
+ resetFormStartedAt(form);
461
483
  validator?.resetLiveState();
462
484
 
463
485
  if (firstPageId) {
464
486
  applyPageState(form, firstPageId);
465
- form.dispatchEvent(new CustomEvent(getFormStateEventName('reset'), { bubbles: true }));
487
+ dispatchFormieDomEvent(form, getFormStateEventName('reset'), undefined);
466
488
  return;
467
489
  }
468
490
 
469
491
  syncPageTabErrors(form);
470
- form.dispatchEvent(new CustomEvent(getFormStateEventName('reset'), { bubbles: true }));
492
+ dispatchFormieDomEvent(form, getFormStateEventName('reset'), undefined);
471
493
  }
472
494
 
473
495
  function shouldResetSubmissionState(result: FormSubmitResult): boolean {
@@ -542,6 +564,12 @@ export function applySubmitResultState(form: HTMLFormElement, result: FormSubmit
542
564
  return;
543
565
  }
544
566
 
567
+ // Provider handlers may navigate as soon as their action event is dispatched.
568
+ // Treat checkout navigation like the generic redirect before invoking them.
569
+ if (result.payment?.action?.type === 'redirect' && result.payment.action.url) {
570
+ markInternalNavigation(form, 'payment-redirect');
571
+ }
572
+
545
573
  // Dispatch payment/redirect events from submitData (e.g. formie:payment:mollie:redirect)
546
574
  const eventDispatchResult = dispatchSubmitDataEvents(form, result);
547
575
 
@@ -363,7 +363,7 @@ function shouldRenderSuccessMessage(form: HTMLFormElement, result: FormSubmitRes
363
363
  return getConfiguredCompletionBehavior(form) === 'message' && getSuccessMessagePosition(form) !== '';
364
364
  }
365
365
 
366
- export function renderFormSuccess(form: HTMLFormElement, message: string): void {
366
+ export function renderFormSuccess(form: HTMLFormElement, message: string, options: { hideForm?: boolean } = {}): void {
367
367
  const position = getSuccessMessagePosition(form);
368
368
 
369
369
  if (!position) {
@@ -384,10 +384,17 @@ export function renderFormSuccess(form: HTMLFormElement, message: string): void
384
384
  successNode.innerHTML = message;
385
385
  container.appendChild(successNode);
386
386
 
387
- if (shouldHideFormOnSuccess(form)) {
387
+ if (options.hideForm !== false && shouldHideFormOnSuccess(form)) {
388
388
  setFormHiddenState(form, true);
389
389
  }
390
390
 
391
+ const scrollToTop = form.dataset.formieScrollToTop?.trim().toLowerCase();
392
+
393
+ if (scrollToTop !== undefined && ['', 'true', '1'].includes(scrollToTop)) {
394
+ // Scroll after hiding the form so the message's final position is used.
395
+ container.scrollIntoView({ behavior: 'smooth', block: 'start' });
396
+ }
397
+
391
398
  const timeoutMs = getSuccessMessageTimeoutMs(form);
392
399
 
393
400
  if (timeoutMs !== null) {
@@ -407,8 +414,23 @@ export function applySubmitResultUi(form: HTMLFormElement, result: FormSubmitRes
407
414
  clearAriaInvalid(form);
408
415
 
409
416
  if (result.ok) {
417
+ const summaryTokens = result.meta?.summaryTokens;
418
+
419
+ // Hidden-page modules mount only after navigation. Persist access before
420
+ // that happens so the new summary instance can read its saved submission.
421
+ if (summaryTokens && typeof summaryTokens === 'object') {
422
+ form.querySelectorAll<HTMLElement>('[data-formie-field-uid]').forEach((field) => {
423
+ const token = (summaryTokens as Record<string, unknown>)[field.dataset.formieFieldUid || ''];
424
+
425
+ if (typeof token === 'string' && token) {
426
+ field.querySelector('[data-formie-summary-container]')?.setAttribute('data-formie-summary-token', token);
427
+ }
428
+ });
429
+ }
430
+
410
431
  if (shouldRenderSuccessMessage(form, result)) {
411
- renderFormSuccess(form, result.message || '');
432
+ // Saving progress acknowledges persistence without completing the form.
433
+ renderFormSuccess(form, result.message || '', { hideForm: result.action !== 'save' });
412
434
  }
413
435
 
414
436
  return;
@@ -14,20 +14,34 @@ function getFirstErroredField(form: HTMLFormElement): HTMLElement | null {
14
14
  }) || null;
15
15
  }
16
16
 
17
+ function getVisibleControl(control: HTMLElement): HTMLElement {
18
+ if (control instanceof HTMLTextAreaElement) {
19
+ const richText = (control as HTMLTextAreaElement & { richText?: { content: HTMLElement } }).richText;
20
+
21
+ if (richText) {
22
+ return richText.content;
23
+ }
24
+ }
25
+
26
+ return control;
27
+ }
28
+
17
29
  function getFieldFocusTarget(field: HTMLElement): HTMLElement | null {
18
30
  const invalidControl = Array.from(field.querySelectorAll('[aria-invalid="true"]')).find((node) => {
19
31
  return !isValidationSkipped(node);
20
32
  }) as HTMLElement | undefined;
21
33
 
22
34
  if (invalidControl) {
23
- return invalidControl;
35
+ return getVisibleControl(invalidControl);
24
36
  }
25
37
 
26
- return Array.from(field.querySelectorAll(
38
+ const control = Array.from(field.querySelectorAll(
27
39
  'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])',
28
40
  )).find((node) => {
29
41
  return !isValidationSkipped(node);
30
- }) as HTMLElement | undefined ?? null;
42
+ }) as HTMLElement | undefined;
43
+
44
+ return control ? getVisibleControl(control) : null;
31
45
  }
32
46
 
33
47
  function getFormLevelErrorTarget(form: HTMLFormElement): HTMLElement | null {
@@ -30,7 +30,8 @@ async function loadGoogleMapsScript(apiKey: string): Promise<GoogleMaps> {
30
30
  if (maps?.places?.PlaceAutocompleteElement) {
31
31
  return existing as GoogleMaps;
32
32
  }
33
- const imp = maps as { importLibrary?: (name: string) => Promise<unknown> } | undefined;
33
+ const imp = maps as
34
+ { importLibrary?: (name: string) => Promise<unknown> } | undefined;
34
35
  if (typeof imp?.importLibrary === 'function') {
35
36
  await imp.importLibrary('places');
36
37
  }
@@ -44,7 +45,8 @@ async function loadGoogleMapsScript(apiKey: string): Promise<GoogleMaps> {
44
45
  const existingScript = document.getElementById(SCRIPT_ID);
45
46
  if (existingScript) {
46
47
  const google = await ensureGlobal<GoogleMaps>('google', 10000);
47
- const maps = google?.maps as { importLibrary?: (name: string) => Promise<unknown> } | undefined;
48
+ const maps = google?.maps as
49
+ { importLibrary?: (name: string) => Promise<unknown> } | undefined;
48
50
  if (typeof maps?.importLibrary === 'function') {
49
51
  await maps.importLibrary('places');
50
52
  }
@@ -80,7 +82,8 @@ async function loadGoogleMapsScript(apiKey: string): Promise<GoogleMaps> {
80
82
  });
81
83
 
82
84
  const google = await ready;
83
- const maps = google?.maps as { importLibrary?: (name: string) => Promise<unknown> } | undefined;
85
+ const maps = google?.maps as
86
+ { importLibrary?: (name: string) => Promise<unknown> } | undefined;
84
87
  if (typeof maps?.importLibrary === 'function') {
85
88
  await maps.importLibrary('places');
86
89
  }
@@ -152,7 +155,13 @@ type AddressComponent = {
152
155
  long_text?: string;
153
156
  };
154
157
 
155
- function parseAddressComponents(components: AddressComponent[]): Record<string, string> {
158
+ type GooglePostalAddress = {
159
+ addressLines?: unknown[];
160
+ };
161
+
162
+ function parseAddressComponents(
163
+ components: AddressComponent[],
164
+ ): Record<string, string> {
156
165
  const map = componentMap();
157
166
  const out: Record<string, string> = {};
158
167
 
@@ -162,10 +171,11 @@ function parseAddressComponents(components: AddressComponent[]): Record<string,
162
171
  if (!type || !map[type as keyof typeof map]) continue;
163
172
 
164
173
  const fieldKey = map[type as keyof typeof map];
165
- const val = (comp as unknown as Record<string, string>)[fieldKey]
166
- ?? comp.short_text
167
- ?? comp.long_text
168
- ?? '';
174
+ const val =
175
+ (comp as unknown as Record<string, string>)[fieldKey] ??
176
+ comp.short_text ??
177
+ comp.long_text ??
178
+ '';
169
179
 
170
180
  out[type] = val;
171
181
  }
@@ -173,17 +183,26 @@ function parseAddressComponents(components: AddressComponent[]): Record<string,
173
183
  return out;
174
184
  }
175
185
 
176
- function buildAddressFromComponents(formData: Record<string, string>): {
186
+ export function buildAddressFromComponents(
187
+ formData: Record<string, string>,
188
+ postalAddress?: GooglePostalAddress,
189
+ ): {
177
190
  address1: string;
178
191
  city: string;
179
192
  state: string;
180
193
  zip: string;
181
194
  country: string;
182
195
  } {
183
- let address1 = '';
196
+ const postalAddressLines = postalAddress?.addressLines?.filter(
197
+ (line): line is string =>
198
+ typeof line === 'string' && line.trim() !== '',
199
+ );
200
+ let address1 = postalAddressLines?.join(', ') || '';
184
201
 
185
- if (formData.street_number || formData.route) {
186
- address1 = [formData.street_number, formData.route].filter(Boolean).join(' ');
202
+ if (!address1 && (formData.street_number || formData.route)) {
203
+ address1 = [formData.street_number, formData.route]
204
+ .filter(Boolean)
205
+ .join(' ');
187
206
 
188
207
  if (formData.subpremise) {
189
208
  address1 = `${formData.subpremise}/${address1}`;
@@ -203,7 +222,8 @@ function hasCountryRestriction(options: Record<string, unknown>): boolean {
203
222
  const componentRestrictions = options.componentRestrictions;
204
223
 
205
224
  if (componentRestrictions && typeof componentRestrictions === 'object') {
206
- const country = (componentRestrictions as { country?: unknown }).country;
225
+ const country = (componentRestrictions as { country?: unknown })
226
+ .country;
207
227
 
208
228
  if (Array.isArray(country) ? country.length > 0 : Boolean(country)) {
209
229
  return true;
@@ -215,7 +235,9 @@ function hasCountryRestriction(options: Record<string, unknown>): boolean {
215
235
  return Array.isArray(includedRegionCodes) && includedRegionCodes.length > 0;
216
236
  }
217
237
 
218
- export function buildGoogleAutocompleteOptions(provider: GoogleAddressProviderOptions): Record<string, unknown> {
238
+ export function buildGoogleAutocompleteOptions(
239
+ provider: GoogleAddressProviderOptions,
240
+ ): Record<string, unknown> {
219
241
  const options = { types: ['geocode'], ...(provider.options || {}) };
220
242
  const country = provider.countryDefaultValue?.trim().toLowerCase();
221
243
 
@@ -247,10 +269,13 @@ export const googleAddressModule = defineAddressModule<
247
269
  },
248
270
  mount: async ({ api, field, services, provider }) => {
249
271
  const input = services.input.getAutocomplete();
250
- const PlaceAutocompleteElement = api?.maps?.places?.PlaceAutocompleteElement;
272
+ const PlaceAutocompleteElement =
273
+ api?.maps?.places?.PlaceAutocompleteElement;
251
274
 
252
275
  if (!input || typeof PlaceAutocompleteElement !== 'function') {
253
- console.warn('[formie] Google Places API not ready; address autocomplete skipped.');
276
+ console.warn(
277
+ '[formie] Google Places API not ready; address autocomplete skipped.',
278
+ );
254
279
  return null;
255
280
  }
256
281
 
@@ -303,20 +328,40 @@ export const googleAddressModule = defineAddressModule<
303
328
  wrapper.appendChild(input);
304
329
 
305
330
  const onSelect = async (ev: Event): Promise<void> => {
306
- const e = ev as unknown as { placePrediction?: { toPlace: () => Promise<{ addressComponents?: unknown[]; formattedAddress?: string; fetchFields: (opts: { fields: string[] }) => Promise<void> }> } };
331
+ const e = ev as unknown as {
332
+ placePrediction?: {
333
+ toPlace: () => Promise<{
334
+ addressComponents?: unknown[];
335
+ formattedAddress?: string;
336
+ postalAddress?: GooglePostalAddress;
337
+ fetchFields: (opts: {
338
+ fields: string[];
339
+ }) => Promise<void>;
340
+ }>;
341
+ };
342
+ };
307
343
  const pred = e.placePrediction;
308
344
 
309
345
  if (!pred) return;
310
346
 
311
347
  const place = await pred.toPlace();
312
- await place.fetchFields({ fields: ['addressComponents', 'formattedAddress'] });
348
+ await place.fetchFields({
349
+ fields: [
350
+ 'addressComponents',
351
+ 'formattedAddress',
352
+ 'postalAddress',
353
+ ],
354
+ });
313
355
 
314
356
  if (!place.addressComponents) return;
315
357
 
316
358
  const formData = parseAddressComponents(
317
359
  place.addressComponents as AddressComponent[],
318
360
  );
319
- const address = buildAddressFromComponents(formData);
361
+ const address = buildAddressFromComponents(
362
+ formData,
363
+ place.postalAddress,
364
+ );
320
365
 
321
366
  setFieldValues(services, {
322
367
  ...address,
@@ -324,15 +369,19 @@ export const googleAddressModule = defineAddressModule<
324
369
  });
325
370
 
326
371
  field.dispatchEvent(
327
- new CustomEvent(getAddressProviderEventName('google', 'populate'), {
328
- bubbles: true,
329
- detail: {
330
- addressProvider: 'google',
331
- place,
332
- formattedAddress: place.formattedAddress,
333
- addressComponents: place.addressComponents,
372
+ new CustomEvent(
373
+ getAddressProviderEventName('google', 'populate'),
374
+ {
375
+ bubbles: true,
376
+ detail: {
377
+ addressProvider: 'google',
378
+ place,
379
+ formattedAddress: place.formattedAddress,
380
+ addressComponents: place.addressComponents,
381
+ postalAddress: place.postalAddress,
382
+ },
334
383
  },
335
- }),
384
+ ),
336
385
  );
337
386
  };
338
387
  autocomplete.addEventListener('gmp-select', onSelect as EventListener);
@@ -342,9 +391,14 @@ export const googleAddressModule = defineAddressModule<
342
391
  onCurrentLocation: async (position, { field, services, provider }) => {
343
392
  const { latitude, longitude } = position.coords;
344
393
  const form = services.form;
345
- const actionUrl = provider.geocodeEndpoint || form?.action || window.location.href;
346
- const fieldHandle = field.getAttribute('data-formie-field-handle')?.trim();
347
- const formHandle = (form?.querySelector('[name="handle"]') as HTMLInputElement | null)?.value?.trim();
394
+ const actionUrl =
395
+ provider.geocodeEndpoint || form?.action || window.location.href;
396
+ const fieldHandle = field
397
+ .getAttribute('data-formie-field-handle')
398
+ ?.trim();
399
+ const formHandle = (
400
+ form?.querySelector('[name="handle"]') as HTMLInputElement | null
401
+ )?.value?.trim();
348
402
 
349
403
  if (!formHandle || !fieldHandle) return;
350
404
 
@@ -356,14 +410,18 @@ export const googleAddressModule = defineAddressModule<
356
410
  formData.append('handle', formHandle);
357
411
  formData.append('fieldHandle', fieldHandle);
358
412
 
359
- const response = await browserRequest(actionUrl, {
360
- method: 'POST',
361
- body: formData,
362
- headers: {
363
- 'X-Requested-With': 'XMLHttpRequest',
364
- Accept: 'application/json',
413
+ const response = await browserRequest(
414
+ actionUrl,
415
+ {
416
+ method: 'POST',
417
+ body: formData,
418
+ headers: {
419
+ 'X-Requested-With': 'XMLHttpRequest',
420
+ Accept: 'application/json',
421
+ },
365
422
  },
366
- }, getFormBrowserRequestOptions(form));
423
+ getFormBrowserRequestOptions(form),
424
+ );
367
425
 
368
426
  const data = await response.json();
369
427
 
@@ -58,13 +58,6 @@ export const placeKitModule = defineAddressModule<
58
58
  const picker = api(provider.apiKey, options as { target: HTMLInputElement });
59
59
 
60
60
  picker.on('pick', (value, item: PlaceKitItem) => {
61
- field.dispatchEvent(
62
- new CustomEvent(getAddressProviderEventName('place-kit', 'populate'), {
63
- bubbles: true,
64
- detail: { addressProvider: 'place-kit', addressComponents: item },
65
- }),
66
- );
67
-
68
61
  let address1 = '';
69
62
 
70
63
  if (item.street) {
@@ -87,6 +80,19 @@ export const placeKitModule = defineAddressModule<
87
80
  services.input.setValue('state', item.administrative || '');
88
81
  services.input.setValue('zip', typeof zip === 'string' ? zip : '');
89
82
  services.input.setValue('country', countryValue || item.country || '');
83
+
84
+ // Notify dependent state controls after all address values are applied.
85
+ // Keep the provider value available while country options are loading.
86
+ field.dispatchEvent(
87
+ new CustomEvent(getAddressProviderEventName('place-kit', 'populate'), {
88
+ bubbles: true,
89
+ detail: {
90
+ addressProvider: 'place-kit',
91
+ addressComponents: item,
92
+ state: item.administrative || '',
93
+ },
94
+ }),
95
+ );
90
96
  });
91
97
 
92
98
  return picker;