@verbb/formie-browser 1.0.13 → 1.0.14

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 (381) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/chunks/{address-country-Rx6GQTND.js → address-country-Dn71jroa.js} +10 -4
  3. package/dist/chunks/{address-finder-k7_BXaRe.js → address-finder-O49uDRoR.js} +2 -2
  4. package/dist/chunks/{address-state-BwE01m2_.js → address-state-ByYatUur.js} +154 -143
  5. package/dist/chunks/{api-DqpfkZWL.js → api-D6kJ7kXZ.js} +183 -179
  6. package/dist/chunks/{api-BUb6P-xu.js → api-DYYsVIub.js} +77 -47
  7. package/dist/chunks/{api-sPqGbOww.js → api-ghNr2uxX.js} +15 -9
  8. package/dist/chunks/{bpoint-Dlq34XH1.js → bpoint-D0RKFM4F.js} +3 -3
  9. package/dist/chunks/calculations-YEoVCG2q.js +310 -0
  10. package/dist/chunks/{captcha-eu-CGm8Lbrj.js → captcha-eu-DERJbXnH.js} +5 -4
  11. package/dist/chunks/{checkbox-radio-BY2aTCOG.js → checkbox-radio-gsm8QPJg.js} +74 -75
  12. package/dist/chunks/{combobox-C0EuExSI.js → combobox-CWNdx4Fk.js} +290 -279
  13. package/dist/chunks/conditions-DqNHLTbN.js +464 -0
  14. package/dist/chunks/country-from-ip-DDj_mFki.js +29 -0
  15. package/dist/chunks/csrf-DxHg_ZYt.js +61 -0
  16. package/dist/chunks/{custom-google-maps-BsaSirEZ.js → custom-google-maps-C4l2S3GO.js} +9 -3
  17. package/dist/chunks/{custom-link-CJ1-FjEM.js → custom-link-CLazgKYH.js} +8 -2
  18. package/dist/chunks/{custom-maps-DYW_pBid.js → custom-maps-DqcAlD0v.js} +10 -4
  19. package/dist/chunks/{date-picker-F9BUBDSW.js → date-picker-RUnevF7l.js} +455 -432
  20. package/dist/chunks/{dist-D6i-VNgM.js → dist-ZLeW0zZ3.js} +1281 -453
  21. package/dist/chunks/{eway-D9_m8ooj.js → eway-or6dtjoT.js} +3 -3
  22. package/dist/chunks/field-references.resolver-Ca8MguFv.js +104 -0
  23. package/dist/chunks/{file-upload-rqfZNqlH.js → file-upload-Bm-TXyNB.js} +104 -96
  24. package/dist/chunks/friendly-captcha-v1-GFqCt1Mh.js +34 -0
  25. package/dist/chunks/{friendly-captcha-v2-CLSAbkqx.js → friendly-captcha-v2-WoW246JG.js} +6 -5
  26. package/dist/chunks/{friendly-challenge-8uPatWqk.js → friendly-challenge-DPG9MJb1.js} +111 -102
  27. package/dist/chunks/{go-cardless-DAT3ZaQf.js → go-cardless-DzJ-0FKg.js} +2 -2
  28. package/dist/chunks/google-address-C1P1nTDM.js +163 -0
  29. package/dist/chunks/{hcaptcha-CGLAmgjO.js → hcaptcha-D0T8aIHz.js} +3 -3
  30. package/dist/chunks/{hidden-D7_Ch-QN.js → hidden-BZhXnNPe.js} +8 -2
  31. package/dist/chunks/http-C2QdHuWO.js +27 -0
  32. package/dist/chunks/{leaflet-src-j-lHNmhR.js → leaflet-src-DE6rv6M-.js} +42 -46
  33. package/dist/chunks/{loqate-CJPzBmSm.js → loqate-DD1S7PY9.js} +2 -2
  34. package/dist/chunks/{mollie-Bibhl67p.js → mollie-DG8Ju83x.js} +2 -2
  35. package/dist/chunks/{moneris-H2wSqk9h.js → moneris-C8m3Fvqu.js} +3 -3
  36. package/dist/chunks/opayo-B3EvrVxw.js +188 -0
  37. package/dist/chunks/{paddle-DkvIxQdG.js → paddle-BIYR9Um3.js} +3 -3
  38. package/dist/chunks/{password-validation-D06uH9va.js → password-validation-C8_uO_c_.js} +18 -12
  39. package/dist/chunks/{paypal-CQpAF7Ua.js → paypal-B_yxIo2d.js} +7 -5
  40. package/dist/chunks/{payway-CwDkC7YZ.js → payway-CeOzS0sL.js} +3 -3
  41. package/dist/chunks/{phone-country-D3ayr60L.js → phone-country-bAqje17P.js} +80 -63
  42. package/dist/chunks/{place-kit-BGHklIZ-.js → place-kit-BE5gi3vo.js} +3 -3
  43. package/dist/chunks/{placekit-autocomplete.esm-DWVabQlq.js → placekit-autocomplete.esm-CZZERYur.js} +11 -15
  44. package/dist/chunks/{recaptcha-enterprise-XPId5-bS.js → recaptcha-enterprise-CruXddZq.js} +4 -4
  45. package/dist/chunks/{recaptcha-shared-CLYJvojk.js → recaptcha-shared-AQRMDMme.js} +1 -1
  46. package/dist/chunks/{recaptcha-v2-checkbox-Cy-0PEPc.js → recaptcha-v2-checkbox-CCnJYCnG.js} +4 -4
  47. package/dist/chunks/{recaptcha-v2-invisible-BwRH540O.js → recaptcha-v2-invisible-BuVqRRYn.js} +4 -4
  48. package/dist/chunks/{recaptcha-v3-No4Ykrhk.js → recaptcha-v3-CloqhO2A.js} +4 -4
  49. package/dist/chunks/{repeater-BVnPUqTj.js → repeater-DwE27jI1.js} +25 -19
  50. package/dist/chunks/request-profile-DhwkeCpS.js +10 -0
  51. package/dist/chunks/rich-text-uV4o_pGk.js +361 -0
  52. package/dist/chunks/{chunk-3b4jIN3o.js → rolldown-runtime-DtPi1Y-2.js} +2 -2
  53. package/dist/chunks/{sdk-BhHAYgga.js → sdk-lkwSvlpM.js} +282 -234
  54. package/dist/chunks/{shared-Bx9s0i0P.js → shared-OKhSay59.js} +27 -21
  55. package/dist/chunks/{signature-IOhuicje.js → signature-7cldFbqj.js} +138 -112
  56. package/dist/chunks/{snaptcha-DKx98oon.js → snaptcha-C04kNGYQ.js} +2 -2
  57. package/dist/chunks/{square-DwdcOI1G.js → square-DEzxv9D9.js} +8 -6
  58. package/dist/chunks/{stripe-DaoG2FT1.js → stripe-CVPQGDz3.js} +10 -11
  59. package/dist/chunks/{summary-Dks0gXS3.js → summary-BF_8mrXI.js} +47 -45
  60. package/dist/chunks/{survey-likert-DkE6bR7B.js → survey-likert-fk1hUPff.js} +7 -1
  61. package/dist/chunks/{survey-rank-DN6x4IZM.js → survey-rank-BsLGl2LX.js} +37 -25
  62. package/dist/chunks/{survey-rating-BnsNL9U5.js → survey-rating-DVS6e6fM.js} +17 -5
  63. package/dist/chunks/{table-CAjCJHrL.js → table-DnIBdeKF.js} +21 -15
  64. package/dist/chunks/{text-limit-BGaY0jhI.js → text-limit-LGMPhFuC.js} +40 -34
  65. package/dist/chunks/{theme-classes-Tv7q7ToE.js → theme-classes-DAQuEqdP.js} +1 -1
  66. package/dist/chunks/{turnstile-DJakPzQD.js → turnstile-n9DNyPxi.js} +3 -3
  67. package/dist/chunks/{upload-manager-W-QDgxKD.js → upload-manager-BbiUosZq.js} +410 -350
  68. package/dist/chunks/{utils-Vqp8i_RM.js → utils-DpUBPadZ.js} +20 -14
  69. package/dist/css/formie-base.css +104 -1
  70. package/dist/css/theme/_messages.css +2 -0
  71. package/dist/css/theme/_tokens.css +31 -25
  72. package/dist/css/theme/forms/_form.css +5 -1
  73. package/dist/index.d.ts +5 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1778 -1427
  76. package/dist/js/contracts/client.d.ts +5 -3
  77. package/dist/js/contracts/client.d.ts.map +1 -1
  78. package/dist/js/contracts/common.d.ts +2 -2
  79. package/dist/js/contracts/common.d.ts.map +1 -1
  80. package/dist/js/contracts/modules.d.ts +37 -12
  81. package/dist/js/contracts/modules.d.ts.map +1 -1
  82. package/dist/js/contracts/schema.d.ts +20 -38
  83. package/dist/js/contracts/schema.d.ts.map +1 -1
  84. package/dist/js/contracts/theme.d.ts +4 -1
  85. package/dist/js/contracts/theme.d.ts.map +1 -1
  86. package/dist/js/core/client-rendered-modules.d.ts +17 -0
  87. package/dist/js/core/client-rendered-modules.d.ts.map +1 -0
  88. package/dist/js/core/create-formie-client.d.ts.map +1 -1
  89. package/dist/js/core/field-error-aria.d.ts +23 -0
  90. package/dist/js/core/field-error-aria.d.ts.map +1 -0
  91. package/dist/js/core/hydrate-modules.d.ts +10 -8
  92. package/dist/js/core/hydrate-modules.d.ts.map +1 -1
  93. package/dist/js/core/submit-result-state.d.ts.map +1 -1
  94. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  95. package/dist/js/core/validation-focus.d.ts.map +1 -1
  96. package/dist/js/modules/address/address-finder.d.ts +1 -1
  97. package/dist/js/modules/address/address-finder.d.ts.map +1 -1
  98. package/dist/js/modules/address/factories.d.ts +3 -3
  99. package/dist/js/modules/address/factories.d.ts.map +1 -1
  100. package/dist/js/modules/address/google-address.d.ts +2 -1
  101. package/dist/js/modules/address/google-address.d.ts.map +1 -1
  102. package/dist/js/modules/address/index.d.ts +2 -2
  103. package/dist/js/modules/address/index.d.ts.map +1 -1
  104. package/dist/js/modules/address/loqate.d.ts +1 -1
  105. package/dist/js/modules/address/loqate.d.ts.map +1 -1
  106. package/dist/js/modules/address/place-kit.d.ts +1 -1
  107. package/dist/js/modules/address/place-kit.d.ts.map +1 -1
  108. package/dist/js/modules/captchas/captcha-eu.d.ts +1 -1
  109. package/dist/js/modules/captchas/captcha-eu.d.ts.map +1 -1
  110. package/dist/js/modules/captchas/factories.d.ts +11 -11
  111. package/dist/js/modules/captchas/factories.d.ts.map +1 -1
  112. package/dist/js/modules/captchas/friendly-captcha-v1.d.ts +1 -1
  113. package/dist/js/modules/captchas/friendly-captcha-v1.d.ts.map +1 -1
  114. package/dist/js/modules/captchas/friendly-captcha-v2.d.ts +1 -1
  115. package/dist/js/modules/captchas/friendly-captcha-v2.d.ts.map +1 -1
  116. package/dist/js/modules/captchas/hcaptcha.d.ts +1 -1
  117. package/dist/js/modules/captchas/hcaptcha.d.ts.map +1 -1
  118. package/dist/js/modules/captchas/index.d.ts +2 -2
  119. package/dist/js/modules/captchas/index.d.ts.map +1 -1
  120. package/dist/js/modules/captchas/recaptcha-enterprise.d.ts +1 -1
  121. package/dist/js/modules/captchas/recaptcha-enterprise.d.ts.map +1 -1
  122. package/dist/js/modules/captchas/recaptcha-v2-checkbox.d.ts +1 -1
  123. package/dist/js/modules/captchas/recaptcha-v2-checkbox.d.ts.map +1 -1
  124. package/dist/js/modules/captchas/recaptcha-v2-invisible.d.ts +1 -1
  125. package/dist/js/modules/captchas/recaptcha-v2-invisible.d.ts.map +1 -1
  126. package/dist/js/modules/captchas/recaptcha-v3.d.ts +1 -1
  127. package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
  128. package/dist/js/modules/captchas/snaptcha.d.ts +1 -1
  129. package/dist/js/modules/captchas/snaptcha.d.ts.map +1 -1
  130. package/dist/js/modules/captchas/turnstile.d.ts +1 -1
  131. package/dist/js/modules/captchas/turnstile.d.ts.map +1 -1
  132. package/dist/js/modules/fields/address-country.d.ts +2 -2
  133. package/dist/js/modules/fields/address-country.d.ts.map +1 -1
  134. package/dist/js/modules/fields/address-state.d.ts +2 -2
  135. package/dist/js/modules/fields/address-state.d.ts.map +1 -1
  136. package/dist/js/modules/fields/calculations.d.ts +2 -2
  137. package/dist/js/modules/fields/calculations.d.ts.map +1 -1
  138. package/dist/js/modules/fields/checkbox-radio.d.ts +2 -2
  139. package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
  140. package/dist/js/modules/fields/combobox.d.ts +2 -2
  141. package/dist/js/modules/fields/combobox.d.ts.map +1 -1
  142. package/dist/js/modules/fields/conditions/config.d.ts.map +1 -1
  143. package/dist/js/modules/fields/conditions/effects.d.ts +1 -0
  144. package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
  145. package/dist/js/modules/fields/conditions/evaluator.d.ts +4 -1
  146. package/dist/js/modules/fields/conditions/evaluator.d.ts.map +1 -1
  147. package/dist/js/modules/fields/conditions/references.d.ts.map +1 -1
  148. package/dist/js/modules/fields/conditions/submission-context.d.ts +15 -0
  149. package/dist/js/modules/fields/conditions/submission-context.d.ts.map +1 -0
  150. package/dist/js/modules/fields/conditions/types.d.ts +3 -1
  151. package/dist/js/modules/fields/conditions/types.d.ts.map +1 -1
  152. package/dist/js/modules/fields/conditions/values.d.ts +1 -1
  153. package/dist/js/modules/fields/conditions/values.d.ts.map +1 -1
  154. package/dist/js/modules/fields/conditions.d.ts +2 -2
  155. package/dist/js/modules/fields/conditions.d.ts.map +1 -1
  156. package/dist/js/modules/fields/custom-google-maps.d.ts +2 -2
  157. package/dist/js/modules/fields/custom-google-maps.d.ts.map +1 -1
  158. package/dist/js/modules/fields/custom-link.d.ts +2 -2
  159. package/dist/js/modules/fields/custom-link.d.ts.map +1 -1
  160. package/dist/js/modules/fields/custom-maps.d.ts +2 -2
  161. package/dist/js/modules/fields/custom-maps.d.ts.map +1 -1
  162. package/dist/js/modules/fields/date-picker.d.ts +2 -2
  163. package/dist/js/modules/fields/date-picker.d.ts.map +1 -1
  164. package/dist/js/modules/fields/file-upload.d.ts +2 -2
  165. package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
  166. package/dist/js/modules/fields/hidden.d.ts +2 -2
  167. package/dist/js/modules/fields/hidden.d.ts.map +1 -1
  168. package/dist/js/modules/fields/index.d.ts +2 -2
  169. package/dist/js/modules/fields/index.d.ts.map +1 -1
  170. package/dist/js/modules/fields/password-validation.d.ts +2 -2
  171. package/dist/js/modules/fields/password-validation.d.ts.map +1 -1
  172. package/dist/js/modules/fields/phone-country.d.ts +2 -2
  173. package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
  174. package/dist/js/modules/fields/repeater.d.ts +2 -2
  175. package/dist/js/modules/fields/repeater.d.ts.map +1 -1
  176. package/dist/js/modules/fields/rich-text.d.ts +2 -2
  177. package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
  178. package/dist/js/modules/fields/shared.d.ts +1 -0
  179. package/dist/js/modules/fields/shared.d.ts.map +1 -1
  180. package/dist/js/modules/fields/signature.d.ts +2 -2
  181. package/dist/js/modules/fields/signature.d.ts.map +1 -1
  182. package/dist/js/modules/fields/summary.d.ts +2 -2
  183. package/dist/js/modules/fields/summary.d.ts.map +1 -1
  184. package/dist/js/modules/fields/survey-likert.d.ts +2 -2
  185. package/dist/js/modules/fields/survey-likert.d.ts.map +1 -1
  186. package/dist/js/modules/fields/survey-rank.d.ts +2 -2
  187. package/dist/js/modules/fields/survey-rank.d.ts.map +1 -1
  188. package/dist/js/modules/fields/survey-rating.d.ts +2 -2
  189. package/dist/js/modules/fields/survey-rating.d.ts.map +1 -1
  190. package/dist/js/modules/fields/table.d.ts +2 -2
  191. package/dist/js/modules/fields/table.d.ts.map +1 -1
  192. package/dist/js/modules/fields/text-limit.d.ts +2 -2
  193. package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
  194. package/dist/js/modules/fields/upload-manager.d.ts +2 -2
  195. package/dist/js/modules/fields/upload-manager.d.ts.map +1 -1
  196. package/dist/js/modules/loader.d.ts +10 -4
  197. package/dist/js/modules/loader.d.ts.map +1 -1
  198. package/dist/js/modules/payments/bpoint.d.ts +1 -1
  199. package/dist/js/modules/payments/bpoint.d.ts.map +1 -1
  200. package/dist/js/modules/payments/eway.d.ts +1 -1
  201. package/dist/js/modules/payments/eway.d.ts.map +1 -1
  202. package/dist/js/modules/payments/factories.d.ts +7 -7
  203. package/dist/js/modules/payments/factories.d.ts.map +1 -1
  204. package/dist/js/modules/payments/go-cardless.d.ts +1 -1
  205. package/dist/js/modules/payments/go-cardless.d.ts.map +1 -1
  206. package/dist/js/modules/payments/index.d.ts +2 -2
  207. package/dist/js/modules/payments/index.d.ts.map +1 -1
  208. package/dist/js/modules/payments/mollie.d.ts +1 -1
  209. package/dist/js/modules/payments/mollie.d.ts.map +1 -1
  210. package/dist/js/modules/payments/moneris.d.ts +1 -1
  211. package/dist/js/modules/payments/moneris.d.ts.map +1 -1
  212. package/dist/js/modules/payments/opayo.d.ts +1 -1
  213. package/dist/js/modules/payments/opayo.d.ts.map +1 -1
  214. package/dist/js/modules/payments/paddle.d.ts +1 -1
  215. package/dist/js/modules/payments/paddle.d.ts.map +1 -1
  216. package/dist/js/modules/payments/paypal.d.ts +1 -1
  217. package/dist/js/modules/payments/paypal.d.ts.map +1 -1
  218. package/dist/js/modules/payments/payway.d.ts +1 -1
  219. package/dist/js/modules/payments/payway.d.ts.map +1 -1
  220. package/dist/js/modules/payments/square.d.ts +1 -1
  221. package/dist/js/modules/payments/square.d.ts.map +1 -1
  222. package/dist/js/modules/payments/stripe.d.ts +1 -1
  223. package/dist/js/modules/payments/stripe.d.ts.map +1 -1
  224. package/dist/js/modules/payments/utils.d.ts +6 -0
  225. package/dist/js/modules/payments/utils.d.ts.map +1 -1
  226. package/dist/js/modules/registry.d.ts +6 -4
  227. package/dist/js/modules/registry.d.ts.map +1 -1
  228. package/dist/js/submit/pipeline.d.ts.map +1 -1
  229. package/dist/js/theme/browser-theme-state.generated.d.ts +24 -0
  230. package/dist/js/theme/browser-theme-state.generated.d.ts.map +1 -0
  231. package/dist/js/theme/theme-classes.d.ts +3 -3
  232. package/dist/js/theme/theme-classes.d.ts.map +1 -1
  233. package/dist/js/transport/forms-api.d.ts +5 -3
  234. package/dist/js/transport/forms-api.d.ts.map +1 -1
  235. package/dist/js/utils/country-from-ip.d.ts +2 -2
  236. package/dist/js/utils/country-from-ip.d.ts.map +1 -1
  237. package/dist/js/utils/csrf.d.ts +17 -0
  238. package/dist/js/utils/csrf.d.ts.map +1 -0
  239. package/dist/js/utils/field-references.parser.d.ts +1 -0
  240. package/dist/js/utils/field-references.parser.d.ts.map +1 -1
  241. package/dist/js/utils/field-references.resolver.d.ts.map +1 -1
  242. package/dist/js/utils/field-references.types.d.ts +1 -0
  243. package/dist/js/utils/field-references.types.d.ts.map +1 -1
  244. package/dist/js/utils/http.d.ts +3 -1
  245. package/dist/js/utils/http.d.ts.map +1 -1
  246. package/dist/js/utils/request-profile.d.ts +4 -0
  247. package/dist/js/utils/request-profile.d.ts.map +1 -0
  248. package/dist/js/utils/unload-warning.d.ts.map +1 -1
  249. package/dist/js/validation/rules/email.d.ts.map +1 -1
  250. package/dist/js/validation/rules/match.d.ts.map +1 -1
  251. package/dist/js/validation/rules/number.d.ts.map +1 -1
  252. package/dist/js/validation/rules/required.d.ts.map +1 -1
  253. package/dist/js/validation/rules/shared.d.ts.map +1 -1
  254. package/dist/js/validation/rules/url.d.ts.map +1 -1
  255. package/dist/js/validation/skip.d.ts +7 -0
  256. package/dist/js/validation/skip.d.ts.map +1 -0
  257. package/dist/js/validation/submit-readiness.d.ts.map +1 -1
  258. package/dist/js/validation/validator.d.ts.map +1 -1
  259. package/package.json +43 -12
  260. package/src/css/formie-base.css +104 -1
  261. package/src/css/theme/_messages.css +2 -0
  262. package/src/css/theme/_tokens.css +31 -25
  263. package/src/css/theme/forms/_form.css +5 -1
  264. package/src/index.ts +13 -8
  265. package/src/js/contracts/client.ts +5 -3
  266. package/src/js/contracts/common.ts +5 -6
  267. package/src/js/contracts/modules.ts +41 -12
  268. package/src/js/contracts/schema.ts +21 -43
  269. package/src/js/contracts/theme.ts +6 -1
  270. package/src/js/core/client-rendered-modules.ts +69 -0
  271. package/src/js/core/create-formie-client.ts +116 -77
  272. package/src/js/core/field-error-aria.ts +93 -0
  273. package/src/js/core/hydrate-modules.ts +54 -29
  274. package/src/js/core/submit-result-state.ts +34 -45
  275. package/src/js/core/submit-result-ui.ts +21 -65
  276. package/src/js/core/validation-focus.ts +20 -20
  277. package/src/js/modules/address/address-finder.ts +1 -1
  278. package/src/js/modules/address/factories.ts +15 -13
  279. package/src/js/modules/address/google-address.ts +10 -6
  280. package/src/js/modules/address/index.ts +2 -2
  281. package/src/js/modules/address/loqate.ts +1 -1
  282. package/src/js/modules/address/place-kit.ts +1 -1
  283. package/src/js/modules/captchas/captcha-eu.ts +2 -2
  284. package/src/js/modules/captchas/factories.ts +66 -42
  285. package/src/js/modules/captchas/friendly-captcha-v1.ts +12 -2
  286. package/src/js/modules/captchas/friendly-captcha-v2.ts +4 -2
  287. package/src/js/modules/captchas/hcaptcha.ts +5 -5
  288. package/src/js/modules/captchas/index.ts +2 -2
  289. package/src/js/modules/captchas/recaptcha-enterprise.ts +2 -2
  290. package/src/js/modules/captchas/recaptcha-v2-checkbox.ts +3 -3
  291. package/src/js/modules/captchas/recaptcha-v2-invisible.ts +2 -2
  292. package/src/js/modules/captchas/recaptcha-v3.ts +2 -2
  293. package/src/js/modules/captchas/snaptcha.ts +1 -1
  294. package/src/js/modules/captchas/turnstile.ts +3 -3
  295. package/src/js/modules/fields/address-country.ts +6 -4
  296. package/src/js/modules/fields/address-state.ts +20 -12
  297. package/src/js/modules/fields/calculations.ts +5 -3
  298. package/src/js/modules/fields/checkbox-radio.ts +24 -42
  299. package/src/js/modules/fields/combobox.ts +25 -9
  300. package/src/js/modules/fields/conditions/config.ts +17 -20
  301. package/src/js/modules/fields/conditions/effects.ts +23 -26
  302. package/src/js/modules/fields/conditions/evaluator.ts +21 -29
  303. package/src/js/modules/fields/conditions/references.ts +16 -5
  304. package/src/js/modules/fields/conditions/submission-context.ts +80 -0
  305. package/src/js/modules/fields/conditions/types.ts +3 -1
  306. package/src/js/modules/fields/conditions/values.ts +31 -6
  307. package/src/js/modules/fields/conditions.ts +59 -18
  308. package/src/js/modules/fields/custom-google-maps.ts +5 -3
  309. package/src/js/modules/fields/custom-link.ts +5 -3
  310. package/src/js/modules/fields/custom-maps.ts +5 -3
  311. package/src/js/modules/fields/date-picker.ts +34 -4
  312. package/src/js/modules/fields/file-upload.ts +19 -10
  313. package/src/js/modules/fields/hidden.ts +5 -3
  314. package/src/js/modules/fields/index.ts +2 -2
  315. package/src/js/modules/fields/password-validation.ts +5 -3
  316. package/src/js/modules/fields/phone-country.ts +33 -6
  317. package/src/js/modules/fields/repeater.ts +5 -3
  318. package/src/js/modules/fields/rich-text.ts +20 -4
  319. package/src/js/modules/fields/shared.ts +13 -0
  320. package/src/js/modules/fields/signature.ts +24 -4
  321. package/src/js/modules/fields/summary.ts +8 -20
  322. package/src/js/modules/fields/survey-likert.ts +5 -3
  323. package/src/js/modules/fields/survey-rank.ts +21 -3
  324. package/src/js/modules/fields/survey-rating.ts +17 -3
  325. package/src/js/modules/fields/table.ts +5 -3
  326. package/src/js/modules/fields/text-limit.ts +5 -3
  327. package/src/js/modules/fields/upload-manager.ts +190 -49
  328. package/src/js/modules/loader.ts +345 -281
  329. package/src/js/modules/payments/bpoint.ts +2 -2
  330. package/src/js/modules/payments/eway.ts +2 -2
  331. package/src/js/modules/payments/factories.ts +53 -52
  332. package/src/js/modules/payments/go-cardless.ts +1 -1
  333. package/src/js/modules/payments/index.ts +2 -2
  334. package/src/js/modules/payments/mollie.ts +1 -1
  335. package/src/js/modules/payments/moneris.ts +2 -2
  336. package/src/js/modules/payments/opayo.ts +21 -8
  337. package/src/js/modules/payments/paddle.ts +10 -6
  338. package/src/js/modules/payments/payment.ts +22 -18
  339. package/src/js/modules/payments/paypal.ts +1 -1
  340. package/src/js/modules/payments/payway.ts +2 -2
  341. package/src/js/modules/payments/square.ts +2 -2
  342. package/src/js/modules/payments/stripe.ts +5 -5
  343. package/src/js/modules/payments/utils.ts +14 -0
  344. package/src/js/modules/registry.ts +19 -11
  345. package/src/js/submit/pipeline.ts +10 -11
  346. package/src/js/theme/browser-theme-state.generated.ts +25 -0
  347. package/src/js/theme/theme-classes.ts +3 -3
  348. package/src/js/transport/forms-api.ts +66 -67
  349. package/src/js/utils/country-from-ip.ts +14 -9
  350. package/src/js/utils/csrf.ts +136 -0
  351. package/src/js/utils/field-references.parser.ts +13 -113
  352. package/src/js/utils/field-references.resolver.ts +27 -64
  353. package/src/js/utils/field-references.row-scope.ts +1 -1
  354. package/src/js/utils/field-references.types.ts +1 -0
  355. package/src/js/utils/http.ts +5 -5
  356. package/src/js/utils/request-profile.ts +10 -0
  357. package/src/js/utils/unload-warning.ts +12 -4
  358. package/src/js/validation/rules/email.ts +3 -5
  359. package/src/js/validation/rules/match.ts +2 -1
  360. package/src/js/validation/rules/minmax.ts +4 -43
  361. package/src/js/validation/rules/number.ts +3 -47
  362. package/src/js/validation/rules/pattern.ts +3 -8
  363. package/src/js/validation/rules/required.ts +2 -1
  364. package/src/js/validation/rules/shared.ts +5 -1
  365. package/src/js/validation/rules/url.ts +3 -10
  366. package/src/js/validation/skip.ts +9 -0
  367. package/src/js/validation/submit-readiness.ts +9 -2
  368. package/src/js/validation/validator.ts +35 -59
  369. package/dist/chunks/calculations-Dv3hYb8_.js +0 -299
  370. package/dist/chunks/conditions-CPH1snCx.js +0 -424
  371. package/dist/chunks/country-from-ip-BwxKaBuJ.js +0 -26
  372. package/dist/chunks/field-references.resolver-CHwn0G0L.js +0 -145
  373. package/dist/chunks/friendly-captcha-v1-C_2reOXr.js +0 -31
  374. package/dist/chunks/google-address-BH3Zhi06.js +0 -161
  375. package/dist/chunks/http-D-JExro7.js +0 -27
  376. package/dist/chunks/opayo-CR0DtsLa.js +0 -179
  377. package/dist/chunks/rich-text-Bg_--4TG.js +0 -349
  378. package/dist/js/modules/fields/conditions/transforms.d.ts +0 -3
  379. package/dist/js/modules/fields/conditions/transforms.d.ts.map +0 -1
  380. package/src/js/modules/fields/conditions/transforms.ts +0 -206
  381. package/src/js/modules/payments/stub-payment-module.ts +0 -87
@@ -1,331 +1,395 @@
1
- import type {
2
- FormieModuleDefinition,
3
- FormieModuleInstance,
4
- ModuleMatchContext,
5
- ModuleSetupContext,
6
- } from '#contracts/modules';
7
- import type { FormModuleManifest, FormModuleTarget, FormModuleTargetType } from '#contracts/schema';
1
+ import { assertBrowserModuleManifest, type BrowserModuleManifest, type BrowserModuleEntry } from '@verbb/formie-core';
2
+ import type { BrowserModuleDefinition, BrowserModuleInstance, BrowserModuleFailure, BrowserModuleHydrationReport, ModuleMatchContext, ModuleSetupContext } from '#contracts/modules';
8
3
  import { builtinAddressModuleLoaders } from '#modules/address';
9
4
  import { builtinCaptchaModuleLoaders } from '#modules/captchas';
10
5
  import { builtinFieldModuleLoaders } from '#modules/fields';
11
6
  import { builtinPaymentModuleLoaders } from '#modules/payments';
12
7
  import { ModuleRegistry } from '#modules/registry';
13
- import { createDebug } from '#utils/debug';
14
- import {
15
- getGlobalModuleLifecycleEventName,
16
- getScopedModuleLifecycleEventName,
17
- type ModuleLifecycleEventPhase,
18
- } from '#utils/event-names';
8
+
9
+ export type BrowserModuleRuntime = BrowserModuleHydrationReport & {
10
+ assertReady: () => void;
11
+ destroy: () => Promise<void>;
12
+ updateManifest: (manifest: BrowserModuleManifest) => Promise<void>;
13
+ };
19
14
 
20
15
  type ModuleLoadContext = {
21
16
  registry: ModuleRegistry;
22
17
  setupContext: ModuleSetupContext;
23
- matchContext: Omit<ModuleMatchContext, 'target' | 'scope' | 'manifestItem'>;
18
+ matchContext: Pick<ModuleMatchContext, 'root' | 'form' | 'surface'>;
24
19
  };
25
20
 
26
- type ModuleDefinitionLoader = () => Promise<FormieModuleDefinition>;
27
-
28
- const builtinModuleLoaders: Record<string, ModuleDefinitionLoader> = {
29
- ...builtinFieldModuleLoaders,
30
- ...builtinAddressModuleLoaders,
31
- ...builtinCaptchaModuleLoaders,
32
- ...builtinPaymentModuleLoaders,
21
+ type ModuleFailureDetails = {
22
+ name: string;
23
+ message: string;
24
+ stack?: string;
33
25
  };
34
26
 
35
- const builtinModuleLoadCache = new Map<string, Promise<FormieModuleDefinition | null>>();
36
- const debug = createDebug('general', 'loader');
37
- const importModuleFromSrc = new Function('src', 'return import(src);') as (src: string) => Promise<unknown>;
38
-
39
- async function emitModuleLifecycleEvent(
40
- emit: ModuleSetupContext['emit'],
41
- moduleId: string,
42
- phase: ModuleLifecycleEventPhase,
43
- detail: Record<string, unknown>,
44
- ): Promise<void> {
45
- // Emit one stable global lifecycle event plus the scoped module event.
46
- // Consumers can subscribe globally and filter by `detail.moduleId`, or bind
47
- // directly to the scoped module id when they need a narrow hook.
48
- await emit(getGlobalModuleLifecycleEventName(phase), detail);
49
- await emit(getScopedModuleLifecycleEventName(moduleId, phase), detail);
50
- }
51
-
52
- function isModuleDefinition(definition: unknown): definition is FormieModuleDefinition {
53
- return !!definition && typeof definition === 'object' &&
54
- typeof (definition as FormieModuleDefinition).id === 'string' &&
55
- typeof (definition as FormieModuleDefinition).setup === 'function' &&
56
- typeof (definition as FormieModuleDefinition).match === 'function';
57
- }
58
-
59
- async function resolveBuiltinDefinition(moduleId: string, ctx: ModuleLoadContext): Promise<FormieModuleDefinition | null> {
60
- const loader = builtinModuleLoaders[moduleId];
27
+ const builtinLoaders = { ...builtinFieldModuleLoaders, ...builtinAddressModuleLoaders, ...builtinCaptchaModuleLoaders, ...builtinPaymentModuleLoaders };
28
+ const pendingDefinitions = new Map<string, Promise<BrowserModuleDefinition>>();
61
29
 
62
- if (!loader) {
63
- return null;
30
+ function normalizeFailureDetails(error: unknown): ModuleFailureDetails {
31
+ if (error instanceof Error) {
32
+ return {
33
+ name: error.name || 'Error',
34
+ message: error.message || 'No error message was provided.',
35
+ stack: error.stack,
36
+ };
64
37
  }
65
38
 
66
- // Cache the in-flight import promise so multiple forms needing the same builtin
67
- // module share one chunk fetch and one registration path.
68
- if (!builtinModuleLoadCache.has(moduleId)) {
69
- builtinModuleLoadCache.set(moduleId, (async() => {
70
- try {
71
- const definition = await loader();
72
-
73
- if (!isModuleDefinition(definition)) {
74
- return null;
75
- }
76
-
77
- ctx.registry.register(definition);
78
-
79
- return definition;
80
- } catch (error) {
81
- console.error('[formie] Failed to load builtin module:', moduleId, error);
82
- debug.warn('Failed loading builtin module.', { moduleId, error });
83
- return null;
84
- }
85
- })());
39
+ if (typeof error === 'string') {
40
+ return { name: 'Error', message: error };
86
41
  }
87
42
 
88
- return builtinModuleLoadCache.get(moduleId) || null;
89
- }
90
-
91
- async function resolveDefinitionFromSrc(src: string): Promise<FormieModuleDefinition | null> {
92
43
  try {
93
- const imported = await importModuleFromSrc(src) as {
94
- default?: unknown;
95
- formieModule?: unknown;
96
- };
97
- const definition = imported?.default || imported?.formieModule || null;
98
-
99
- if (!isModuleDefinition(definition)) {
100
- return null;
101
- }
102
-
103
- return definition;
104
- } catch (error) {
105
- console.error('[formie] Failed to load module from src:', src, error);
106
- debug.warn('Failed loading module from src.', { src, error });
107
- return null;
44
+ return { name: 'Error', message: JSON.stringify(error) || String(error) };
45
+ } catch {
46
+ return { name: 'Error', message: String(error) };
108
47
  }
109
48
  }
110
49
 
111
- async function resolveDefinition(manifestItem: FormModuleManifest, ctx: ModuleLoadContext): Promise<FormieModuleDefinition | null> {
112
- const registered = ctx.registry.get(manifestItem.id);
113
-
114
- // Resolution order is deliberate:
115
- // 1) already-registered custom/client-side modules
116
- // 2) first-party lazy builtin modules
117
- // 3) explicit manifest src imports
118
- if (registered) {
119
- return registered;
50
+ function buildTechnicalDetails(surface: BrowserModuleManifest['surface'], failures: Iterable<BrowserModuleFailure>, details: Map<string, ModuleFailureDetails>): string {
51
+ const requiredFailures = [...failures].filter((failure) => failure.required);
52
+ const lines = [
53
+ 'Formie browser module diagnostics',
54
+ `Surface: ${surface}`,
55
+ `Browser: ${typeof navigator === 'undefined' ? 'Unavailable' : navigator.userAgent}`,
56
+ ];
57
+
58
+ // Query parameters can contain resume or edit capabilities. Keep the useful
59
+ // CP path without copying those credentials into the diagnostic output.
60
+ if (surface === 'cp-edit' && typeof location !== 'undefined') {
61
+ lines.push(`Page: ${location.origin}${location.pathname}`);
120
62
  }
121
63
 
122
- const builtin = await resolveBuiltinDefinition(manifestItem.id, ctx);
123
-
124
- if (builtin) {
125
- return builtin;
126
- }
127
-
128
- if (manifestItem.src) {
129
- const fromSrc = await resolveDefinitionFromSrc(manifestItem.src);
130
-
131
- if (fromSrc) {
132
- ctx.registry.register(fromSrc);
133
- return fromSrc;
64
+ requiredFailures.forEach((failure, index) => {
65
+ const failureDetails = details.get(failure.key);
66
+ lines.push(
67
+ '',
68
+ `Failure ${index + 1}`,
69
+ `Module: ${failure.moduleId}`,
70
+ `Key: ${failure.key}`,
71
+ `Code: ${failure.code}`,
72
+ );
73
+
74
+ // Raw exceptions can disclose implementation details, so only expose
75
+ // them in Craft's authenticated submission-editing surface.
76
+ if (surface === 'cp-edit' && failureDetails) {
77
+ lines.push(`Error: ${failureDetails.name}: ${failureDetails.message}`);
78
+
79
+ if (failureDetails.stack) {
80
+ lines.push('Stack:', failureDetails.stack);
81
+ }
134
82
  }
135
- }
83
+ });
136
84
 
137
- return null;
85
+ return lines.join('\n');
138
86
  }
139
87
 
140
- function escapeSelectorValue(value: string): string {
141
- if (typeof window.CSS?.escape === 'function') {
142
- return window.CSS.escape(value);
88
+ async function copyTechnicalDetails(value: string): Promise<void> {
89
+ if (navigator.clipboard?.writeText) {
90
+ await navigator.clipboard.writeText(value);
91
+ return;
143
92
  }
144
93
 
145
- return value.replace(/["\\]/g, '\\$&');
146
- }
94
+ const textarea = document.createElement('textarea');
95
+ textarea.value = value;
96
+ textarea.setAttribute('readonly', '');
97
+ textarea.style.position = 'fixed';
98
+ textarea.style.opacity = '0';
99
+ document.body.appendChild(textarea);
100
+ textarea.select();
147
101
 
148
- function queryTargets(root: Element, selector: string): Element[] {
149
- if (root.matches(selector)) {
150
- return [root, ...Array.from(root.querySelectorAll(selector))];
102
+ try {
103
+ if (!document.execCommand('copy')) {
104
+ throw new Error('The browser did not allow clipboard access.');
105
+ }
106
+ } finally {
107
+ textarea.remove();
151
108
  }
152
-
153
- return Array.from(root.querySelectorAll(selector));
154
109
  }
155
110
 
156
- function resolveTarget(target: FormModuleTarget, ctx: ModuleLoadContext): Array<{ scope: FormModuleTargetType; element: Element }> {
157
- const root = ctx.setupContext.root;
158
- const form = ctx.setupContext.form;
159
- const scope = target.targetType;
160
- const targetId = target.targetId;
161
-
162
- // Manifest targeting is resolved centrally so module implementations receive
163
- // a concrete DOM surface instead of re-querying ownership from options.
164
- if (scope === 'selector') {
165
- return queryTargets(root, targetId).map((element) => {
166
- return { scope, element };
167
- });
168
- }
169
-
170
- if (scope === 'field') {
171
- return queryTargets(root, `[data-formie-field-handle="${escapeSelectorValue(targetId)}"]`).map((element) => {
172
- return { scope, element };
173
- });
174
- }
175
-
176
- if (scope === 'page') {
177
- return queryTargets(root, `[data-formie-page-id="${escapeSelectorValue(targetId)}"]`).map((element) => {
178
- return { scope, element };
179
- });
180
- }
181
-
182
- if (scope === 'button') {
183
- return queryTargets(root, `[data-formie-action="${escapeSelectorValue(targetId)}"]`).map((element) => {
184
- return { scope, element };
185
- });
111
+ async function resolveDefinition(moduleId: string, registry: ModuleRegistry): Promise<BrowserModuleDefinition> {
112
+ const registered = registry.get(moduleId);
113
+ if (registered) return registered;
114
+ const loader = moduleId.startsWith('formie:') && Object.prototype.hasOwnProperty.call(builtinLoaders, moduleId.slice(7)) ? builtinLoaders[moduleId.slice(7)] : undefined;
115
+ if (!loader) throw new Error(`Browser module ${moduleId} is not registered.`);
116
+ if (!pendingDefinitions.has(moduleId)) {
117
+ pendingDefinitions.set(moduleId, loader().catch((error) => { pendingDefinitions.delete(moduleId); throw error; }));
186
118
  }
187
-
188
- return [{
189
- scope: 'form',
190
- element: form || root,
191
- }];
119
+ const definition = await pendingDefinitions.get(moduleId)!;
120
+ if (definition.moduleId !== moduleId) throw new Error(`Module definition does not match ${moduleId}.`);
121
+ registry.register(definition);
122
+ return definition;
192
123
  }
193
124
 
194
- function resolveTargets(item: FormModuleManifest, ctx: ModuleLoadContext): Array<{ scope: FormModuleTargetType; element: Element }> {
195
- const targets: FormModuleTarget[] = item.targets && item.targets.length > 0 ? item.targets : [{
196
- targetType: 'form',
197
- targetId: 'form',
198
- }];
199
-
200
- return targets.flatMap((target) => {
201
- return resolveTarget(target, ctx);
202
- });
125
+ function resolveTargets(entry: BrowserModuleEntry, root: Element, form: HTMLFormElement | null): Element[] {
126
+ return [...new Set(entry.targets.flatMap((target) => {
127
+ if (target.type === 'form') return [form || root];
128
+ const selector = target.type === 'selector'
129
+ ? target.selector
130
+ : target.type === 'field'
131
+ ? `[data-formie-field-uid="${CSS.escape(target.uid)}"]`
132
+ : target.type === 'page'
133
+ ? `[data-formie-page-id="${CSS.escape(target.id)}"]`
134
+ : `[data-formie-action="${CSS.escape(target.action)}"]`;
135
+ return [...(root.matches(selector) ? [root] : []), ...root.querySelectorAll(selector)];
136
+ }))].filter((target) => !target.closest('[hidden], [data-formie-hidden="true"], [data-formie-conditionally-hidden], [data-formie-page-hidden]'));
203
137
  }
204
138
 
205
- export async function loadModulesFromManifest(
206
- manifest: FormModuleManifest[],
207
- ctx: ModuleLoadContext,
208
- ): Promise<FormieModuleInstance[]> {
209
- const instances: FormieModuleInstance[] = [];
210
- debug.log('Loading module manifest.', {
211
- manifestCount: manifest.length,
212
- });
139
+ /** Reconcile declaration key + DOM occurrence, including targets added by repeaters. */
140
+ export async function loadModulesFromManifest(manifest: BrowserModuleManifest, ctx: ModuleLoadContext): Promise<BrowserModuleRuntime> {
141
+ assertBrowserModuleManifest(manifest);
142
+ const surface = manifest.surface;
143
+ if (ctx.matchContext.surface !== surface || ctx.setupContext.surface !== surface) {
144
+ throw new Error(`Browser module manifest surface ${surface} cannot mount as ${ctx.matchContext.surface}.`);
145
+ }
146
+ const { root, form } = ctx.setupContext;
147
+ const mounted = new Map<string, Map<Element, { instance: BrowserModuleInstance; config: string; moduleId: string; required: boolean }>>();
148
+ const failures = new Map<string, BrowserModuleFailure>();
149
+ const failureDetails = new Map<string, ModuleFailureDetails>();
150
+ const instances: BrowserModuleInstance[] = [];
151
+ let disposed = false;
152
+ let running: Promise<void> = Promise.resolve();
153
+ let queued = false;
154
+ const diagnose = async(entry: BrowserModuleEntry, error: unknown) => {
155
+ const diagnostic: BrowserModuleFailure = { key: entry.key, moduleId: entry.moduleId, required: entry.required, surface, code: 'MODULE_UNAVAILABLE', message: 'A form feature could not start. Reload the page or contact the site administrator.' };
156
+ failures.set(entry.key, diagnostic);
157
+ failureDetails.set(entry.key, normalizeFailureDetails(error));
158
+ console.error('[formie] Browser module failure', diagnostic, error);
159
+ await ctx.setupContext.emit('formie:browser:module:error', diagnostic);
160
+ };
161
+ const dispose = async(instance: BrowserModuleInstance) => {
162
+ try { await instance.destroy(); }
163
+ catch (error) {
164
+ console.error('[formie] Browser module disposal failed', error);
165
+ await ctx.setupContext.emit('formie:browser:module:error', { key: instance.key, moduleId: instance.moduleId, surface, code: 'MODULE_DISPOSE_FAILED', message: 'A form feature could not clean up. Reload the page before continuing.' });
166
+ }
167
+ };
168
+ const blocked = () => [...failures.values()].some((entry) => entry.required);
169
+ const showFailure = () => {
170
+ if (!form) return;
171
+ let message = form.querySelector<HTMLElement>('[data-formie-module-error]');
172
+
173
+ if (!message) {
174
+ const description = document.createElement('p');
175
+ description.textContent = surface === 'cp-edit'
176
+ ? 'Reload the page. If the problem continues, copy the technical details below.'
177
+ : 'Reload the page or contact the site administrator.';
178
+
179
+ const report = document.createElement('pre');
180
+ report.dataset.formieModuleErrorDetails = 'true';
181
+ report.tabIndex = 0;
182
+
183
+ if (surface === 'cp-edit') {
184
+ message = document.createElement('pk-alert');
185
+ message.className = 'formie-module-error';
186
+ message.setAttribute('variant', 'error');
187
+ message.setAttribute('announce', 'assertive');
188
+ message.setAttribute('heading', 'A required form feature could not start.');
189
+ message.setAttribute('details-label', 'Details');
190
+ message.setAttribute('copy-label', 'Copy details');
191
+ message.setAttribute('copied-label', 'Details copied.');
192
+ message.setAttribute('copyable', '');
193
+ report.slot = 'details';
194
+ message.append(description, report);
195
+ } else {
196
+ message = document.createElement('div');
197
+ message.className = 'formie-module-error formie-module-error--fallback';
198
+
199
+ const notice = document.createElement('div');
200
+ notice.className = 'formie-module-error__notice';
201
+ notice.setAttribute('role', 'alert');
202
+
203
+ const indicator = document.createElement('span');
204
+ indicator.className = 'formie-module-error__indicator';
205
+ indicator.setAttribute('aria-hidden', 'true');
206
+ indicator.textContent = '!';
207
+
208
+ const content = document.createElement('div');
209
+ content.className = 'formie-module-error__content';
210
+
211
+ const title = document.createElement('strong');
212
+ title.className = 'formie-module-error__title';
213
+ title.textContent = 'A required form feature could not start.';
214
+
215
+ content.append(title, description);
216
+ notice.append(indicator, content);
217
+
218
+ const disclosure = document.createElement('details');
219
+ disclosure.className = 'formie-module-error__details';
220
+
221
+ const summary = document.createElement('summary');
222
+ summary.textContent = 'Details';
223
+
224
+ const actions = document.createElement('div');
225
+ actions.className = 'formie-module-error__actions';
226
+
227
+ const copyButton = document.createElement('button');
228
+ copyButton.className = 'formie-module-error__copy';
229
+ copyButton.type = 'button';
230
+ copyButton.textContent = 'Copy details';
231
+
232
+ const copyStatus = document.createElement('span');
233
+ copyStatus.className = 'formie-module-error__copy-status';
234
+ copyStatus.setAttribute('role', 'status');
235
+ copyStatus.setAttribute('aria-live', 'polite');
236
+
237
+ copyButton.addEventListener('click', async() => {
238
+ try {
239
+ await copyTechnicalDetails(report.textContent || '');
240
+ copyStatus.textContent = 'Details copied.';
241
+ } catch {
242
+ copyStatus.textContent = 'Copy failed. Select the details above and copy them manually.';
243
+ const range = document.createRange();
244
+ range.selectNodeContents(report);
245
+ window.getSelection()?.removeAllRanges();
246
+ window.getSelection()?.addRange(range);
247
+ }
248
+ });
213
249
 
214
- for (const item of manifest) {
215
- const definition = await resolveDefinition(item, ctx);
250
+ actions.append(copyButton, copyStatus);
251
+ disclosure.append(summary, report, actions);
252
+ message.append(notice, disclosure);
253
+ }
216
254
 
217
- if (!definition) {
218
- debug.warn('Skipping manifest item (definition not resolved).', {
219
- moduleId: item.id,
220
- src: item.src,
221
- });
222
- continue;
223
- }
255
+ message.dataset.formieModuleError = 'true';
224
256
 
225
- const targets = resolveTargets(item, ctx);
226
- debug.log('Resolved module targets.', {
227
- moduleId: definition.id,
228
- targets: item.targets || [],
229
- targetCount: targets.length,
230
- });
231
-
232
- if (targets.length === 0 && definition.kind === 'address') {
233
- console.warn(
234
- `[formie] Address module "${item.id}" skipped: no target element found for ` +
235
- `fieldHandle="${item.targets?.find((target) => target.targetType === 'field')?.targetId ?? '?'}". ` +
236
- 'Check that the Address field exists in the rendered form.',
237
- );
257
+ // CP edit roots live inside Craft's padded content pane. Insert beside
258
+ // that root so the alert follows the same alignment as the fields.
259
+ if (root !== form && form.contains(root)) {
260
+ root.before(message);
261
+ } else {
262
+ form.prepend(message);
263
+ }
238
264
  }
239
265
 
240
- for (const target of targets) {
241
- const matchContext: ModuleMatchContext = {
242
- ...ctx.matchContext,
243
- target: target.element,
244
- scope: target.scope,
245
- manifestItem: item,
246
- };
247
-
248
- if (!definition.match(matchContext)) {
249
- if (definition.kind === 'address') {
250
- console.warn(
251
- `[formie] Address module "${definition.id}" skipped: target element does not contain ` +
252
- '[data-formie-address-autocomplete-input]. Enable the Auto-Complete subfield.',
253
- );
266
+ const report = message.querySelector<HTMLElement>('[data-formie-module-error-details]');
267
+ const details = buildTechnicalDetails(surface, failures.values(), failureDetails);
268
+ // This report lives inside the observed root; unchanged text must not
269
+ // schedule another reconciliation while a module remains unavailable.
270
+ if (report && report.textContent !== details) report.textContent = details;
271
+ };
272
+ const guard = (event: Event) => {
273
+ if (!blocked()) return;
274
+ event.preventDefault(); event.stopImmediatePropagation(); showFailure();
275
+ };
276
+ const reconcile = async() => {
277
+ const activeKeys = new Set(manifest.entries.map((entry) => entry.key));
278
+ for (const key of failures.keys()) {
279
+ if (activeKeys.has(key)) continue;
280
+ failures.delete(key);
281
+ failureDetails.delete(key);
282
+ }
283
+ const inactiveInstances: BrowserModuleInstance[] = [];
284
+ for (const [key, records] of mounted) {
285
+ if (activeKeys.has(key)) continue;
286
+ inactiveInstances.push(...Array.from(records.values(), ({ instance }) => instance));
287
+ mounted.delete(key);
288
+ failures.delete(key);
289
+ failureDetails.delete(key);
290
+ }
291
+ for (const instance of inactiveInstances.reverse()) {
292
+ await dispose(instance);
293
+ instances.splice(instances.indexOf(instance), 1);
294
+ }
295
+ for (const entry of manifest.entries) {
296
+ if (disposed) continue;
297
+ const failure = failures.get(entry.key);
298
+ if (failure) failures.set(entry.key, { ...failure, required: entry.required });
299
+ let targets: Element[];
300
+ try { targets = resolveTargets(entry, root, form); }
301
+ catch (error) { if (!failures.has(entry.key)) await diagnose(entry, error); continue; }
302
+ const records = mounted.get(entry.key) ?? new Map();
303
+ mounted.set(entry.key, records);
304
+ for (const [target, record] of Array.from(records.entries()).reverse()) {
305
+ if (!targets.includes(target)) {
306
+ await dispose(record.instance); records.delete(target);
307
+ instances.splice(instances.indexOf(record.instance), 1);
254
308
  }
255
- debug.log('Module target did not match predicate.', {
256
- moduleId: definition.id,
257
- scope: target.scope,
258
- });
259
- continue;
260
309
  }
261
-
262
- const options = item.config || ctx.setupContext.options;
263
- const moduleEventName = definition.id;
264
- const lifecycleDetail = {
265
- moduleId: definition.id,
266
- moduleKind: definition.kind,
267
- target: target.element,
268
- scope: target.scope,
269
- options,
270
- manifestItem: item,
271
- };
272
-
273
- await emitModuleLifecycleEvent(ctx.setupContext.emit, moduleEventName, 'before-setup', lifecycleDetail);
274
-
275
- let instance: FormieModuleInstance | null = null;
276
-
277
- try {
278
- const setupResult = await definition.setup({
279
- ...ctx.setupContext,
280
- target: target.element,
281
- scope: target.scope,
282
- options,
283
- });
284
-
285
- if (setupResult) {
286
- instance = setupResult;
287
- }
288
- } catch (err) {
289
- console.error(`[formie] Module "${definition.id}" setup failed:`, err);
290
- debug.warn('Module setup failed.', {
291
- moduleId: definition.id,
292
- scope: target.scope,
293
- error: err,
294
- });
310
+ // Resolve even absent targets: unknown required declarations cannot disappear silently.
311
+ let definition: BrowserModuleDefinition;
312
+ try { definition = await resolveDefinition(entry.moduleId, ctx.registry); }
313
+ catch (error) { if (!failures.has(entry.key)) await diagnose(entry, error); continue; }
314
+ let entryFailed = false;
315
+ let recovered = false;
316
+ for (const target of targets) {
317
+ if (disposed) return;
318
+ const config = JSON.stringify([entry.moduleId, entry.config, entry.required]);
319
+ const existing = records.get(target);
320
+ if (existing?.config === config) continue;
321
+ const setup = { ...ctx.setupContext, target, entryKey: entry.key, surface, scope: entry.targets[0]?.type ?? 'form', options: entry.config };
322
+ try {
323
+ if (existing) {
324
+ if (existing.instance.update && existing.moduleId === entry.moduleId && existing.required === entry.required) { await existing.instance.update(setup); existing.config = config; continue; }
325
+ await dispose(existing.instance); records.delete(target);
326
+ instances.splice(instances.indexOf(existing.instance), 1);
327
+ }
328
+ if (definition.surfaces && !definition.surfaces.includes(surface)) throw new Error(`Module ${entry.moduleId} does not support ${surface}.`);
329
+ if (definition.kind !== entry.kind) throw new Error(`Module ${entry.moduleId} is registered as ${definition.kind}, not ${entry.kind}.`);
330
+ if (!definition.match({ ...ctx.matchContext, target, scope: setup.scope, manifestItem: entry })) {
331
+ throw new Error(`Module ${entry.moduleId} does not support the rendered target.`);
332
+ }
333
+ const instance = await definition.setup(setup);
334
+ if (!instance) throw new Error(`Module ${entry.moduleId} did not initialize.`);
335
+ if (disposed || !root.contains(target) && target !== root) { await dispose(instance); continue; }
336
+ instance.key = entry.key; instance.moduleId = entry.moduleId; instance.kind = entry.kind; instance.target = target;
337
+ const ready = instance.assertReady;
338
+ instance.assertReady = () => {
339
+ try { ready?.(); }
340
+ catch (error) { void diagnose(entry, error); if (entry.required) throw new Error('A required form feature could not start.'); }
341
+ };
342
+ const before = instance.beforeSubmit;
343
+ const after = instance.afterSubmit;
344
+ instance.beforeSubmit = async(context) => {
345
+ try { await before?.(context); }
346
+ catch (error) { await diagnose(entry, error); if (entry.required) context.abort('A required form feature could not complete. Reload the page or contact the site administrator.'); }
347
+ };
348
+ instance.afterSubmit = async(context, result) => {
349
+ try { await after?.(context, result); }
350
+ catch (error) { await diagnose(entry, error); }
351
+ };
352
+ records.set(target, { instance, config, moduleId: entry.moduleId, required: entry.required }); instances.push(instance); recovered = true;
353
+ await ctx.setupContext.emit('formie:browser:module:mount', { key: entry.key, moduleId: entry.moduleId, target });
354
+ } catch (error) { entryFailed = true; if (!failures.has(entry.key)) await diagnose(entry, error); }
295
355
  }
296
-
297
- await emitModuleLifecycleEvent(ctx.setupContext.emit, moduleEventName, 'after-setup', {
298
- ...lifecycleDetail,
299
- instanceCreated: !!instance,
300
- });
301
-
302
- if (instance) {
303
- debug.log('Module instance created.', {
304
- moduleId: definition.id,
305
- scope: target.scope,
306
- });
307
- instances.push({
308
- ...instance,
309
- destroy: async() => {
310
- debug.log('Destroying module instance.', {
311
- moduleId: definition.id,
312
- scope: target.scope,
313
- });
314
- await emitModuleLifecycleEvent(ctx.setupContext.emit, moduleEventName, 'before-destroy', lifecycleDetail);
315
- await instance.destroy();
316
- await emitModuleLifecycleEvent(ctx.setupContext.emit, moduleEventName, 'after-destroy', lifecycleDetail);
317
- debug.log('Module instance destroyed.', {
318
- moduleId: definition.id,
319
- scope: target.scope,
320
- });
321
- },
322
- });
356
+ if (!entryFailed && (recovered || targets.length === 0)) {
357
+ failures.delete(entry.key);
358
+ failureDetails.delete(entry.key);
323
359
  }
324
360
  }
325
- }
326
-
327
- debug.log('Module manifest processing complete.', {
328
- instanceCount: instances.length,
329
- });
330
- return instances;
361
+ if (blocked()) showFailure(); else form?.querySelector('[data-formie-module-error]')?.remove();
362
+ };
363
+ const schedule = () => {
364
+ if (queued || disposed) return;
365
+ queued = true;
366
+ running = running.then(async() => { queued = false; if (!disposed) await reconcile(); });
367
+ void running.catch((error) => console.error('[formie] Module reconciliation failed', error));
368
+ };
369
+ const observer = new MutationObserver(schedule);
370
+ const runtime: BrowserModuleRuntime = {
371
+ get instances() { return instances; },
372
+ get failures() { return [...failures.values()]; },
373
+ assertReady: () => { if (blocked()) throw new Error('A required form feature could not start. Reload the page or contact the site administrator.'); },
374
+ destroy: async() => {
375
+ disposed = true; observer.disconnect(); form?.removeEventListener('submit', guard, true);
376
+ await running;
377
+ for (const records of Array.from(mounted.values()).reverse()) {
378
+ for (const { instance } of Array.from(records.values()).reverse()) await dispose(instance);
379
+ }
380
+ mounted.clear(); instances.splice(0); failures.clear(); failureDetails.clear();
381
+ form?.querySelector('[data-formie-module-error]')?.remove();
382
+ },
383
+ updateManifest: async(next) => {
384
+ assertBrowserModuleManifest(next);
385
+ if (next.surface !== surface) throw new Error('A mounted browser module runtime cannot change surfaces.');
386
+ manifest = next;
387
+ running = running.then(reconcile);
388
+ await running;
389
+ },
390
+ };
391
+ form?.addEventListener('submit', guard, true);
392
+ await reconcile();
393
+ observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'data-formie-hidden', 'data-formie-conditionally-hidden', 'data-formie-page-hidden', 'data-formie-field-uid', 'data-formie-page-id', 'data-formie-action'] });
394
+ return runtime;
331
395
  }