@verbb/formie-browser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE.md +40 -0
- package/README.md +34 -0
- package/dist/chunks/address-finder-DfMCiW89.js +47 -0
- package/dist/chunks/api-CbqEMQT5.js +181 -0
- package/dist/chunks/api-DE7LfK-R.js +960 -0
- package/dist/chunks/api-DOfDzYC_.js +538 -0
- package/dist/chunks/async-B3DUf1GZ.js +26 -0
- package/dist/chunks/bpoint-Ciy3yY9Q.js +34 -0
- package/dist/chunks/calculations-CkYAqO_-.js +197 -0
- package/dist/chunks/captcha-eu-DnOWhMwr.js +43 -0
- package/dist/chunks/checkbox-radio-0x7Tc0br.js +197 -0
- package/dist/chunks/chunk-K6L4z4UQ.js +24 -0
- package/dist/chunks/conditions-4fXKhEJS.js +609 -0
- package/dist/chunks/date-picker-B6iZkjHS.js +6204 -0
- package/dist/chunks/debug-KnZeKYBI.js +39 -0
- package/dist/chunks/dist-D09GnXMW.js +2663 -0
- package/dist/chunks/event-names-DamGPtXR.js +51 -0
- package/dist/chunks/eway-DEAYcwT0.js +86 -0
- package/dist/chunks/field-references.keys-BpBZ_quS.js +24 -0
- package/dist/chunks/field-references.resolver-Ba6xhiJC.js +183 -0
- package/dist/chunks/file-upload-Bh63PQSE.js +430 -0
- package/dist/chunks/friendly-captcha-v1-CqO4WVre.js +40 -0
- package/dist/chunks/friendly-captcha-v2-CyykcJcM.js +47 -0
- package/dist/chunks/friendly-challenge-Dg8XkStd.js +1101 -0
- package/dist/chunks/go-cardless-CuND59rR.js +26 -0
- package/dist/chunks/google-address--uR8WDSm.js +208 -0
- package/dist/chunks/hcaptcha-CmaFUesv.js +72 -0
- package/dist/chunks/hidden-CYnZYple.js +36 -0
- package/dist/chunks/http-18nn97DZ.js +29 -0
- package/dist/chunks/i18n-vUh-KGiH.js +55 -0
- package/dist/chunks/loqate-BICNJlVK.js +97 -0
- package/dist/chunks/mollie-DwlsgHZ1.js +26 -0
- package/dist/chunks/moneris-B_IFZFTx.js +159 -0
- package/dist/chunks/opayo-U2x_TOII.js +192 -0
- package/dist/chunks/paddle-BqXFrc79.js +75 -0
- package/dist/chunks/paypal-Cn_DYGDb.js +121 -0
- package/dist/chunks/payway-Rnq796eC.js +75 -0
- package/dist/chunks/phone-country-B6Me4lK0.js +3317 -0
- package/dist/chunks/place-kit-ldUl-u9w.js +56 -0
- package/dist/chunks/placekit-autocomplete.esm-D-lGeaZl.js +1771 -0
- package/dist/chunks/recaptcha-enterprise-DPJNyv1X.js +72 -0
- package/dist/chunks/recaptcha-shared-DTI4qWVR.js +22 -0
- package/dist/chunks/recaptcha-v2-checkbox-zFjpvJ5c.js +49 -0
- package/dist/chunks/recaptcha-v2-invisible-CnYtkNvz.js +62 -0
- package/dist/chunks/recaptcha-v3-EAlWhnkX.js +33 -0
- package/dist/chunks/repeater-CXD1eLSn.js +151 -0
- package/dist/chunks/rich-text-DkmZRhGj.js +442 -0
- package/dist/chunks/scripts-BGD_iU_6.js +41 -0
- package/dist/chunks/sdk-B7u9fTlP.js +2103 -0
- package/dist/chunks/shared-DC6_1u8X.js +85 -0
- package/dist/chunks/signature-E9KyYXS1.js +765 -0
- package/dist/chunks/snaptcha-CCDunGeb.js +8 -0
- package/dist/chunks/square-BLqK51rS.js +61 -0
- package/dist/chunks/stripe-B8gHpZNC.js +273 -0
- package/dist/chunks/styles-BIh6g7V_.js +22 -0
- package/dist/chunks/summary-EcNE0cvg.js +191 -0
- package/dist/chunks/table-yxEDL6kA.js +124 -0
- package/dist/chunks/text-limit-D0H_Ca2c.js +179 -0
- package/dist/chunks/theme-classes-vSHpdCUO.js +59 -0
- package/dist/chunks/turnstile-DP0bdR7T.js +68 -0
- package/dist/chunks/utils-ByrEVYrJ.js +49584 -0
- package/dist/css/formie-base.css +78 -0
- package/dist/css/formie-theme.css +19 -0
- package/dist/css/formie.css +2 -0
- package/dist/css/theme/_buttons.css +249 -0
- package/dist/css/theme/_loading.css +37 -0
- package/dist/css/theme/_messages.css +39 -0
- package/dist/css/theme/_progress.css +62 -0
- package/dist/css/theme/_tokens.css +361 -0
- package/dist/css/theme/_typography.css +70 -0
- package/dist/css/theme/fields/_address.css +17 -0
- package/dist/css/theme/fields/_check-radio.css +108 -0
- package/dist/css/theme/fields/_file.css +58 -0
- package/dist/css/theme/fields/_group.css +13 -0
- package/dist/css/theme/fields/_input.css +48 -0
- package/dist/css/theme/fields/_nested.css +19 -0
- package/dist/css/theme/fields/_repeater.css +69 -0
- package/dist/css/theme/fields/_rich-text.css +201 -0
- package/dist/css/theme/fields/_select.css +37 -0
- package/dist/css/theme/fields/_signature.css +39 -0
- package/dist/css/theme/fields/_summary.css +53 -0
- package/dist/css/theme/fields/_table.css +121 -0
- package/dist/css/theme/fields/_text-limit.css +10 -0
- package/dist/css/theme/forms/_field.css +62 -0
- package/dist/css/theme/forms/_form.css +166 -0
- package/dist/css/theme/integrations/_payment-modal.css +32 -0
- package/dist/css/theme/integrations/_paypal.css +10 -0
- package/dist/css/theme/integrations/_payway.css +10 -0
- package/dist/css/theme/integrations/_stripe.css +24 -0
- package/dist/css/theme/utilities/_accessibility.css +13 -0
- package/dist/css/theme-base/_controls.css +41 -0
- package/dist/css/theme-base/_primitives.css +34 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3206 -0
- package/dist/js/compatibility/dom-adapter.d.ts +12 -0
- package/dist/js/compatibility/dom-adapter.d.ts.map +1 -0
- package/dist/js/compatibility/event-map.d.ts +25 -0
- package/dist/js/compatibility/event-map.d.ts.map +1 -0
- package/dist/js/compatibility/validator-adapter.d.ts +17 -0
- package/dist/js/compatibility/validator-adapter.d.ts.map +1 -0
- package/dist/js/contracts/client.d.ts +40 -0
- package/dist/js/contracts/client.d.ts.map +1 -0
- package/dist/js/contracts/common.d.ts +5 -0
- package/dist/js/contracts/common.d.ts.map +1 -0
- package/dist/js/contracts/modules.d.ts +47 -0
- package/dist/js/contracts/modules.d.ts.map +1 -0
- package/dist/js/contracts/schema.d.ts +79 -0
- package/dist/js/contracts/schema.d.ts.map +1 -0
- package/dist/js/contracts/theme.d.ts +2 -0
- package/dist/js/contracts/theme.d.ts.map +1 -0
- package/dist/js/core/create-formie-client.d.ts +3 -0
- package/dist/js/core/create-formie-client.d.ts.map +1 -0
- package/dist/js/core/dom-events.d.ts +2 -0
- package/dist/js/core/dom-events.d.ts.map +1 -0
- package/dist/js/core/formie.d.ts +27 -0
- package/dist/js/core/formie.d.ts.map +1 -0
- package/dist/js/core/hydrate-modules.d.ts +22 -0
- package/dist/js/core/hydrate-modules.d.ts.map +1 -0
- package/dist/js/core/page-client-event.d.ts +9 -0
- package/dist/js/core/page-client-event.d.ts.map +1 -0
- package/dist/js/core/page-tab-errors.d.ts +2 -0
- package/dist/js/core/page-tab-errors.d.ts.map +1 -0
- package/dist/js/core/submit-flow.d.ts +21 -0
- package/dist/js/core/submit-flow.d.ts.map +1 -0
- package/dist/js/core/submit-result-state.d.ts +8 -0
- package/dist/js/core/submit-result-state.d.ts.map +1 -0
- package/dist/js/core/submit-result-ui.d.ts +10 -0
- package/dist/js/core/submit-result-ui.d.ts.map +1 -0
- package/dist/js/events/event-bus.d.ts +21 -0
- package/dist/js/events/event-bus.d.ts.map +1 -0
- package/dist/js/modules/address/address-finder.d.ts +2 -0
- package/dist/js/modules/address/address-finder.d.ts.map +1 -0
- package/dist/js/modules/address/api.d.ts +8 -0
- package/dist/js/modules/address/api.d.ts.map +1 -0
- package/dist/js/modules/address/constants.d.ts +15 -0
- package/dist/js/modules/address/constants.d.ts.map +1 -0
- package/dist/js/modules/address/factories.d.ts +35 -0
- package/dist/js/modules/address/factories.d.ts.map +1 -0
- package/dist/js/modules/address/google-address.d.ts +2 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -0
- package/dist/js/modules/address/host.d.ts +30 -0
- package/dist/js/modules/address/host.d.ts.map +1 -0
- package/dist/js/modules/address/index.d.ts +3 -0
- package/dist/js/modules/address/index.d.ts.map +1 -0
- package/dist/js/modules/address/loqate.d.ts +2 -0
- package/dist/js/modules/address/loqate.d.ts.map +1 -0
- package/dist/js/modules/address/place-kit.d.ts +2 -0
- package/dist/js/modules/address/place-kit.d.ts.map +1 -0
- package/dist/js/modules/captchas/api.d.ts +9 -0
- package/dist/js/modules/captchas/api.d.ts.map +1 -0
- package/dist/js/modules/captchas/captcha-eu.d.ts +2 -0
- package/dist/js/modules/captchas/captcha-eu.d.ts.map +1 -0
- package/dist/js/modules/captchas/constants.d.ts +5 -0
- package/dist/js/modules/captchas/constants.d.ts.map +1 -0
- package/dist/js/modules/captchas/factories.d.ts +63 -0
- package/dist/js/modules/captchas/factories.d.ts.map +1 -0
- package/dist/js/modules/captchas/friendly-captcha-v1.d.ts +2 -0
- package/dist/js/modules/captchas/friendly-captcha-v1.d.ts.map +1 -0
- package/dist/js/modules/captchas/friendly-captcha-v2.d.ts +2 -0
- package/dist/js/modules/captchas/friendly-captcha-v2.d.ts.map +1 -0
- package/dist/js/modules/captchas/hcaptcha.d.ts +2 -0
- package/dist/js/modules/captchas/hcaptcha.d.ts.map +1 -0
- package/dist/js/modules/captchas/host.d.ts +78 -0
- package/dist/js/modules/captchas/host.d.ts.map +1 -0
- package/dist/js/modules/captchas/index.d.ts +3 -0
- package/dist/js/modules/captchas/index.d.ts.map +1 -0
- package/dist/js/modules/captchas/recaptcha-enterprise.d.ts +2 -0
- package/dist/js/modules/captchas/recaptcha-enterprise.d.ts.map +1 -0
- package/dist/js/modules/captchas/recaptcha-shared.d.ts +26 -0
- package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -0
- package/dist/js/modules/captchas/recaptcha-v2-checkbox.d.ts +2 -0
- package/dist/js/modules/captchas/recaptcha-v2-checkbox.d.ts.map +1 -0
- package/dist/js/modules/captchas/recaptcha-v2-invisible.d.ts +2 -0
- package/dist/js/modules/captchas/recaptcha-v2-invisible.d.ts.map +1 -0
- package/dist/js/modules/captchas/recaptcha-v3.d.ts +2 -0
- package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -0
- package/dist/js/modules/captchas/snaptcha.d.ts +2 -0
- package/dist/js/modules/captchas/snaptcha.d.ts.map +1 -0
- package/dist/js/modules/captchas/turnstile.d.ts +2 -0
- package/dist/js/modules/captchas/turnstile.d.ts.map +1 -0
- package/dist/js/modules/captchas/utils.d.ts +13 -0
- package/dist/js/modules/captchas/utils.d.ts.map +1 -0
- package/dist/js/modules/fields/calculations.d.ts +3 -0
- package/dist/js/modules/fields/calculations.d.ts.map +1 -0
- package/dist/js/modules/fields/checkbox-radio.d.ts +3 -0
- package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/config.d.ts +5 -0
- package/dist/js/modules/fields/conditions/config.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/effects.d.ts +2 -0
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/evaluator.d.ts +6 -0
- package/dist/js/modules/fields/conditions/evaluator.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/references.d.ts +5 -0
- package/dist/js/modules/fields/conditions/references.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/transforms.d.ts +3 -0
- package/dist/js/modules/fields/conditions/transforms.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/types.d.ts +30 -0
- package/dist/js/modules/fields/conditions/types.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions/values.d.ts +5 -0
- package/dist/js/modules/fields/conditions/values.d.ts.map +1 -0
- package/dist/js/modules/fields/conditions.d.ts +3 -0
- package/dist/js/modules/fields/conditions.d.ts.map +1 -0
- package/dist/js/modules/fields/date-picker.d.ts +3 -0
- package/dist/js/modules/fields/date-picker.d.ts.map +1 -0
- package/dist/js/modules/fields/file-upload.d.ts +3 -0
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -0
- package/dist/js/modules/fields/hidden.d.ts +3 -0
- package/dist/js/modules/fields/hidden.d.ts.map +1 -0
- package/dist/js/modules/fields/index.d.ts +3 -0
- package/dist/js/modules/fields/index.d.ts.map +1 -0
- package/dist/js/modules/fields/phone-country.d.ts +3 -0
- package/dist/js/modules/fields/phone-country.d.ts.map +1 -0
- package/dist/js/modules/fields/repeater.d.ts +3 -0
- package/dist/js/modules/fields/repeater.d.ts.map +1 -0
- package/dist/js/modules/fields/rich-text.d.ts +3 -0
- package/dist/js/modules/fields/rich-text.d.ts.map +1 -0
- package/dist/js/modules/fields/shared.d.ts +20 -0
- package/dist/js/modules/fields/shared.d.ts.map +1 -0
- package/dist/js/modules/fields/signature.d.ts +3 -0
- package/dist/js/modules/fields/signature.d.ts.map +1 -0
- package/dist/js/modules/fields/summary.d.ts +3 -0
- package/dist/js/modules/fields/summary.d.ts.map +1 -0
- package/dist/js/modules/fields/table.d.ts +3 -0
- package/dist/js/modules/fields/table.d.ts.map +1 -0
- package/dist/js/modules/fields/text-limit.d.ts +3 -0
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -0
- package/dist/js/modules/loader.d.ts +11 -0
- package/dist/js/modules/loader.d.ts.map +1 -0
- package/dist/js/modules/payments/api.d.ts +8 -0
- package/dist/js/modules/payments/api.d.ts.map +1 -0
- package/dist/js/modules/payments/bpoint.d.ts +2 -0
- package/dist/js/modules/payments/bpoint.d.ts.map +1 -0
- package/dist/js/modules/payments/constants.d.ts +5 -0
- package/dist/js/modules/payments/constants.d.ts.map +1 -0
- package/dist/js/modules/payments/eway.d.ts +9 -0
- package/dist/js/modules/payments/eway.d.ts.map +1 -0
- package/dist/js/modules/payments/factories.d.ts +54 -0
- package/dist/js/modules/payments/factories.d.ts.map +1 -0
- package/dist/js/modules/payments/go-cardless.d.ts +2 -0
- package/dist/js/modules/payments/go-cardless.d.ts.map +1 -0
- package/dist/js/modules/payments/host.d.ts +70 -0
- package/dist/js/modules/payments/host.d.ts.map +1 -0
- package/dist/js/modules/payments/index.d.ts +3 -0
- package/dist/js/modules/payments/index.d.ts.map +1 -0
- package/dist/js/modules/payments/mollie.d.ts +2 -0
- package/dist/js/modules/payments/mollie.d.ts.map +1 -0
- package/dist/js/modules/payments/moneris.d.ts +2 -0
- package/dist/js/modules/payments/moneris.d.ts.map +1 -0
- package/dist/js/modules/payments/opayo.d.ts +25 -0
- package/dist/js/modules/payments/opayo.d.ts.map +1 -0
- package/dist/js/modules/payments/paddle.d.ts +2 -0
- package/dist/js/modules/payments/paddle.d.ts.map +1 -0
- package/dist/js/modules/payments/paypal.d.ts +2 -0
- package/dist/js/modules/payments/paypal.d.ts.map +1 -0
- package/dist/js/modules/payments/payway.d.ts +2 -0
- package/dist/js/modules/payments/payway.d.ts.map +1 -0
- package/dist/js/modules/payments/square.d.ts +2 -0
- package/dist/js/modules/payments/square.d.ts.map +1 -0
- package/dist/js/modules/payments/stripe.d.ts +2 -0
- package/dist/js/modules/payments/stripe.d.ts.map +1 -0
- package/dist/js/modules/payments/utils.d.ts +17 -0
- package/dist/js/modules/payments/utils.d.ts.map +1 -0
- package/dist/js/modules/registry.d.ts +9 -0
- package/dist/js/modules/registry.d.ts.map +1 -0
- package/dist/js/modules/styles.d.ts +2 -0
- package/dist/js/modules/styles.d.ts.map +1 -0
- package/dist/js/submit/pipeline.d.ts +18 -0
- package/dist/js/submit/pipeline.d.ts.map +1 -0
- package/dist/js/theme/theme-classes.d.ts +10 -0
- package/dist/js/theme/theme-classes.d.ts.map +1 -0
- package/dist/js/transport/forms-api.d.ts +11 -0
- package/dist/js/transport/forms-api.d.ts.map +1 -0
- package/dist/js/utils/async.d.ts +8 -0
- package/dist/js/utils/async.d.ts.map +1 -0
- package/dist/js/utils/debug.d.ts +10 -0
- package/dist/js/utils/debug.d.ts.map +1 -0
- package/dist/js/utils/event-names.d.ts +13 -0
- package/dist/js/utils/event-names.d.ts.map +1 -0
- package/dist/js/utils/field-references.d.ts +6 -0
- package/dist/js/utils/field-references.d.ts.map +1 -0
- package/dist/js/utils/field-references.keys.d.ts +4 -0
- package/dist/js/utils/field-references.keys.d.ts.map +1 -0
- package/dist/js/utils/field-references.parser.d.ts +3 -0
- package/dist/js/utils/field-references.parser.d.ts.map +1 -0
- package/dist/js/utils/field-references.registry.d.ts +3 -0
- package/dist/js/utils/field-references.registry.d.ts.map +1 -0
- package/dist/js/utils/field-references.resolver.d.ts +4 -0
- package/dist/js/utils/field-references.resolver.d.ts.map +1 -0
- package/dist/js/utils/field-references.types.d.ts +27 -0
- package/dist/js/utils/field-references.types.d.ts.map +1 -0
- package/dist/js/utils/fields.d.ts +5 -0
- package/dist/js/utils/fields.d.ts.map +1 -0
- package/dist/js/utils/http.d.ts +9 -0
- package/dist/js/utils/http.d.ts.map +1 -0
- package/dist/js/utils/i18n.d.ts +7 -0
- package/dist/js/utils/i18n.d.ts.map +1 -0
- package/dist/js/utils/scripts.d.ts +13 -0
- package/dist/js/utils/scripts.d.ts.map +1 -0
- package/dist/js/utils/unload-warning.d.ts +10 -0
- package/dist/js/utils/unload-warning.d.ts.map +1 -0
- package/dist/js/validation/rules/email.d.ts +4 -0
- package/dist/js/validation/rules/email.d.ts.map +1 -0
- package/dist/js/validation/rules/match.d.ts +4 -0
- package/dist/js/validation/rules/match.d.ts.map +1 -0
- package/dist/js/validation/rules/number.d.ts +4 -0
- package/dist/js/validation/rules/number.d.ts.map +1 -0
- package/dist/js/validation/rules/required.d.ts +4 -0
- package/dist/js/validation/rules/required.d.ts.map +1 -0
- package/dist/js/validation/rules/shared.d.ts +7 -0
- package/dist/js/validation/rules/shared.d.ts.map +1 -0
- package/dist/js/validation/rules/url.d.ts +4 -0
- package/dist/js/validation/rules/url.d.ts.map +1 -0
- package/dist/js/validation/rules.d.ts +10 -0
- package/dist/js/validation/rules.d.ts.map +1 -0
- package/dist/js/validation/types.d.ts +44 -0
- package/dist/js/validation/types.d.ts.map +1 -0
- package/dist/js/validation/validator.d.ts +53 -0
- package/dist/js/validation/validator.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/css/formie-base.css +78 -0
- package/src/css/formie-theme.css +19 -0
- package/src/css/formie.css +2 -0
- package/src/css/theme/_buttons.css +249 -0
- package/src/css/theme/_loading.css +37 -0
- package/src/css/theme/_messages.css +39 -0
- package/src/css/theme/_progress.css +62 -0
- package/src/css/theme/_tokens.css +361 -0
- package/src/css/theme/_typography.css +70 -0
- package/src/css/theme/fields/_address.css +17 -0
- package/src/css/theme/fields/_check-radio.css +108 -0
- package/src/css/theme/fields/_file.css +58 -0
- package/src/css/theme/fields/_group.css +13 -0
- package/src/css/theme/fields/_input.css +48 -0
- package/src/css/theme/fields/_nested.css +19 -0
- package/src/css/theme/fields/_repeater.css +69 -0
- package/src/css/theme/fields/_rich-text.css +201 -0
- package/src/css/theme/fields/_select.css +37 -0
- package/src/css/theme/fields/_signature.css +39 -0
- package/src/css/theme/fields/_summary.css +53 -0
- package/src/css/theme/fields/_table.css +121 -0
- package/src/css/theme/fields/_text-limit.css +10 -0
- package/src/css/theme/forms/_field.css +62 -0
- package/src/css/theme/forms/_form.css +166 -0
- package/src/css/theme/integrations/_payment-modal.css +32 -0
- package/src/css/theme/integrations/_paypal.css +10 -0
- package/src/css/theme/integrations/_payway.css +10 -0
- package/src/css/theme/integrations/_stripe.css +24 -0
- package/src/css/theme/utilities/_accessibility.css +13 -0
- package/src/css/theme-base/_controls.css +41 -0
- package/src/css/theme-base/_primitives.css +34 -0
- package/src/icons/rich-text/aligncenter.svg +6 -0
- package/src/icons/rich-text/alignleft.svg +6 -0
- package/src/icons/rich-text/alignright.svg +6 -0
- package/src/icons/rich-text/bold.svg +4 -0
- package/src/icons/rich-text/clear.svg +6 -0
- package/src/icons/rich-text/code.svg +4 -0
- package/src/icons/rich-text/heading1.svg +3 -0
- package/src/icons/rich-text/heading2.svg +3 -0
- package/src/icons/rich-text/image.svg +6 -0
- package/src/icons/rich-text/italic.svg +5 -0
- package/src/icons/rich-text/line.svg +3 -0
- package/src/icons/rich-text/link.svg +4 -0
- package/src/icons/rich-text/olist.svg +8 -0
- package/src/icons/rich-text/paragraph.svg +3 -0
- package/src/icons/rich-text/quote.svg +4 -0
- package/src/icons/rich-text/strikethrough.svg +4 -0
- package/src/icons/rich-text/ulist.svg +8 -0
- package/src/icons/rich-text/underline.svg +4 -0
- package/src/index.ts +125 -0
- package/src/js/compatibility/dom-adapter.ts +129 -0
- package/src/js/compatibility/event-map.ts +72 -0
- package/src/js/compatibility/validator-adapter.ts +105 -0
- package/src/js/contracts/client.ts +43 -0
- package/src/js/contracts/common.ts +14 -0
- package/src/js/contracts/modules.ts +53 -0
- package/src/js/contracts/schema.ts +83 -0
- package/src/js/contracts/theme.ts +1 -0
- package/src/js/core/create-formie-client.ts +1519 -0
- package/src/js/core/dom-events.ts +8 -0
- package/src/js/core/formie.ts +242 -0
- package/src/js/core/hydrate-modules.ts +102 -0
- package/src/js/core/page-client-event.ts +129 -0
- package/src/js/core/page-tab-errors.ts +37 -0
- package/src/js/core/submit-flow.ts +120 -0
- package/src/js/core/submit-result-state.ts +597 -0
- package/src/js/core/submit-result-ui.ts +448 -0
- package/src/js/events/event-bus.ts +109 -0
- package/src/js/modules/address/address-finder.ts +85 -0
- package/src/js/modules/address/api.ts +22 -0
- package/src/js/modules/address/constants.ts +15 -0
- package/src/js/modules/address/factories.ts +203 -0
- package/src/js/modules/address/google-address.ts +345 -0
- package/src/js/modules/address/host.ts +137 -0
- package/src/js/modules/address/index.ts +10 -0
- package/src/js/modules/address/loqate.ts +128 -0
- package/src/js/modules/address/place-kit.ts +94 -0
- package/src/js/modules/captchas/api.ts +25 -0
- package/src/js/modules/captchas/captcha-eu.ts +86 -0
- package/src/js/modules/captchas/constants.ts +4 -0
- package/src/js/modules/captchas/factories.ts +485 -0
- package/src/js/modules/captchas/friendly-captcha-v1.ts +65 -0
- package/src/js/modules/captchas/friendly-captcha-v2.ts +84 -0
- package/src/js/modules/captchas/hcaptcha.ts +153 -0
- package/src/js/modules/captchas/host.ts +448 -0
- package/src/js/modules/captchas/index.ts +16 -0
- package/src/js/modules/captchas/recaptcha-enterprise.ts +138 -0
- package/src/js/modules/captchas/recaptcha-shared.ts +69 -0
- package/src/js/modules/captchas/recaptcha-v2-checkbox.ts +72 -0
- package/src/js/modules/captchas/recaptcha-v2-invisible.ts +108 -0
- package/src/js/modules/captchas/recaptcha-v3.ts +62 -0
- package/src/js/modules/captchas/snaptcha.ts +10 -0
- package/src/js/modules/captchas/turnstile.ts +131 -0
- package/src/js/modules/captchas/utils.ts +85 -0
- package/src/js/modules/fields/calculations.ts +273 -0
- package/src/js/modules/fields/checkbox-radio.ts +295 -0
- package/src/js/modules/fields/conditions/config.ts +79 -0
- package/src/js/modules/fields/conditions/effects.ts +166 -0
- package/src/js/modules/fields/conditions/evaluator.ts +44 -0
- package/src/js/modules/fields/conditions/references.ts +165 -0
- package/src/js/modules/fields/conditions/transforms.ts +206 -0
- package/src/js/modules/fields/conditions/types.ts +33 -0
- package/src/js/modules/fields/conditions/values.ts +115 -0
- package/src/js/modules/fields/conditions.ts +229 -0
- package/src/js/modules/fields/date-picker.ts +272 -0
- package/src/js/modules/fields/file-upload.ts +628 -0
- package/src/js/modules/fields/hidden.ts +58 -0
- package/src/js/modules/fields/index.ts +19 -0
- package/src/js/modules/fields/phone-country.ts +226 -0
- package/src/js/modules/fields/repeater.ts +231 -0
- package/src/js/modules/fields/rich-text.ts +217 -0
- package/src/js/modules/fields/shared.ts +238 -0
- package/src/js/modules/fields/signature.ts +202 -0
- package/src/js/modules/fields/summary.ts +272 -0
- package/src/js/modules/fields/table.ts +197 -0
- package/src/js/modules/fields/text-limit.ts +280 -0
- package/src/js/modules/loader.ts +331 -0
- package/src/js/modules/payments/api.ts +20 -0
- package/src/js/modules/payments/bpoint.ts +48 -0
- package/src/js/modules/payments/constants.ts +17 -0
- package/src/js/modules/payments/eway.ts +132 -0
- package/src/js/modules/payments/factories.ts +332 -0
- package/src/js/modules/payments/go-cardless.ts +37 -0
- package/src/js/modules/payments/host.ts +459 -0
- package/src/js/modules/payments/index.ts +17 -0
- package/src/js/modules/payments/mollie.ts +38 -0
- package/src/js/modules/payments/moneris.ts +216 -0
- package/src/js/modules/payments/opayo.ts +272 -0
- package/src/js/modules/payments/paddle.ts +111 -0
- package/src/js/modules/payments/payment.ts +183 -0
- package/src/js/modules/payments/paypal.ts +214 -0
- package/src/js/modules/payments/payway.ts +114 -0
- package/src/js/modules/payments/square.ts +106 -0
- package/src/js/modules/payments/stripe.ts +426 -0
- package/src/js/modules/payments/stub-payment-module.ts +87 -0
- package/src/js/modules/payments/utils.ts +60 -0
- package/src/js/modules/registry.ts +38 -0
- package/src/js/modules/styles.ts +29 -0
- package/src/js/submit/pipeline.ts +514 -0
- package/src/js/theme/theme-classes.ts +106 -0
- package/src/js/transport/forms-api.ts +345 -0
- package/src/js/utils/async.ts +81 -0
- package/src/js/utils/debug.ts +59 -0
- package/src/js/utils/event-names.ts +60 -0
- package/src/js/utils/field-references.keys.ts +47 -0
- package/src/js/utils/field-references.parser.ts +121 -0
- package/src/js/utils/field-references.registry.ts +50 -0
- package/src/js/utils/field-references.resolver.ts +115 -0
- package/src/js/utils/field-references.ts +11 -0
- package/src/js/utils/field-references.types.ts +31 -0
- package/src/js/utils/fields.ts +58 -0
- package/src/js/utils/http.ts +51 -0
- package/src/js/utils/i18n.ts +98 -0
- package/src/js/utils/scripts.ts +84 -0
- package/src/js/utils/unload-warning.ts +190 -0
- package/src/js/validation/rules/email.ts +18 -0
- package/src/js/validation/rules/match.ts +26 -0
- package/src/js/validation/rules/minmax.ts +47 -0
- package/src/js/validation/rules/number.ts +55 -0
- package/src/js/validation/rules/pattern.ts +29 -0
- package/src/js/validation/rules/required.ts +30 -0
- package/src/js/validation/rules/shared.ts +47 -0
- package/src/js/validation/rules/url.ts +23 -0
- package/src/js/validation/rules.ts +16 -0
- package/src/js/validation/types.ts +50 -0
- package/src/js/validation/validator.ts +643 -0
- package/src/vendor.d.ts +100 -0
- package/src/vite-env.d.ts +22 -0
- package/vite-dev.mjs +22 -0
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FormieClient,
|
|
3
|
+
FormieFormInstance,
|
|
4
|
+
FormMountOptions,
|
|
5
|
+
} from '#contracts/client';
|
|
6
|
+
import { bindLegacyDomEventCompatibility } from '#compatibility/dom-adapter';
|
|
7
|
+
import { resolveLegacyCompatibilityOptions } from '#compatibility/event-map';
|
|
8
|
+
import { bindLegacyValidatorCompatibility } from '#compatibility/validator-adapter';
|
|
9
|
+
import type { FormAction, FormMode, FormTransport } from '#contracts/common';
|
|
10
|
+
import type { FormieModuleDefinition, FormieModuleInstance } from '#contracts/modules';
|
|
11
|
+
import type { FormEndpointPayload, FormModuleManifest, FormSubmitResult } from '#contracts/schema';
|
|
12
|
+
import type { ThemeClassMap } from '#contracts/theme';
|
|
13
|
+
import { dispatchFormieDomEvent } from '#core/dom-events';
|
|
14
|
+
import { dispatchPageClientEventForSubmit } from '#core/page-client-event';
|
|
15
|
+
import { syncPageTabErrors } from '#core/page-tab-errors';
|
|
16
|
+
import { clearSubmitFeedback, executeAjaxSubmitFlow, shouldKeepSubmitLoading } from '#core/submit-flow';
|
|
17
|
+
import { applySubmitResultUi } from '#core/submit-result-ui';
|
|
18
|
+
import { applyPageState, clearSubmitLoading, setSubmitLoading } from '#core/submit-result-state';
|
|
19
|
+
import { EventBus } from '#events/event-bus';
|
|
20
|
+
import { ModuleRegistry } from '#modules/registry';
|
|
21
|
+
import { loadModulesFromManifest } from '#modules/loader';
|
|
22
|
+
import { registerThemeClassMap } from '#theme/theme-classes';
|
|
23
|
+
import { runSubmitPipeline } from '#submit/pipeline';
|
|
24
|
+
import { clearSubmissionOnUnload, requestGraphqlRender, requestRefreshTokens, requestRender, requestSetPage } from '#transport/forms-api';
|
|
25
|
+
import { createDebug } from '#utils/debug';
|
|
26
|
+
import { createFormUnloadWarningGuard } from '#utils/unload-warning';
|
|
27
|
+
import { FormieValidator } from '#validation/validator';
|
|
28
|
+
|
|
29
|
+
type InternalInstanceState = {
|
|
30
|
+
options: FormMountOptions;
|
|
31
|
+
bus: EventBus;
|
|
32
|
+
form: HTMLFormElement | null;
|
|
33
|
+
validator: FormieValidator | null;
|
|
34
|
+
modules: FormieModuleInstance[];
|
|
35
|
+
unbinds: Array<() => void>;
|
|
36
|
+
instance: FormieFormInstance;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type FormWithValidationApi = HTMLFormElement & {
|
|
40
|
+
formieValidation?: FormieValidator;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ROOT_SELECTORS = '[data-formie]:not([data-formie-init="false"]), [data-formie-form]:not([data-formie-init="false"])';
|
|
44
|
+
const DEFAULT_SUBMIT_DELAY_MS = 300;
|
|
45
|
+
const DEFAULT_HEADLESS_RENDER_ACTION = '/actions/formie/server/forms/render';
|
|
46
|
+
const DEFAULT_HEADLESS_GRAPHQL_ENDPOINT = '/api';
|
|
47
|
+
const DEFAULT_HEADLESS_REFRESH_TOKENS_ACTION = '/actions/formie/server/forms/refresh-tokens';
|
|
48
|
+
const DEFAULT_HEADLESS_SUBMIT_ACTION = '/actions/formie/server/submissions/submit';
|
|
49
|
+
const DEFAULT_HEADLESS_SET_PAGE_ACTION = '/actions/formie/server/submissions/set-page';
|
|
50
|
+
const DEFAULT_HEADLESS_CLEAR_SUBMISSION_ACTION = '/actions/formie/server/submissions/clear-submission';
|
|
51
|
+
const DEFAULT_FILE_UPLOAD_HYDRATE_ACTION = '/actions/formie/file-upload/hydrate';
|
|
52
|
+
const debug = createDebug('general', 'client');
|
|
53
|
+
const compatibilityWarnings = new Set<string>();
|
|
54
|
+
|
|
55
|
+
function parseBooleanOption(value: string | undefined, defaultValue: boolean): boolean {
|
|
56
|
+
if (value == null || value === '') {
|
|
57
|
+
return defaultValue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalized = value.toLowerCase();
|
|
61
|
+
|
|
62
|
+
return !(normalized === 'false' || normalized === '0' || normalized === 'off');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inferStaticCacheOnLoadFromDataset(dataset: DOMStringMap): boolean {
|
|
66
|
+
if (dataset.formieRefreshTokens != null && dataset.formieRefreshTokens !== '') {
|
|
67
|
+
return parseBooleanOption(dataset.formieRefreshTokens, false);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (dataset.formieStaticCache != null && dataset.formieStaticCache !== '') {
|
|
71
|
+
return parseBooleanOption(dataset.formieStaticCache, false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inferOptionsFromElement(target: Element): FormMountOptions {
|
|
78
|
+
const dataset = target instanceof HTMLElement ? target.dataset : ({} as DOMStringMap);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
mode: 'server-rendered',
|
|
82
|
+
transport: (dataset.formieTransport as FormTransport) || 'rest',
|
|
83
|
+
formHandle: dataset.formieHandle,
|
|
84
|
+
endpoint: dataset.formieEndpoint,
|
|
85
|
+
staticCache: inferStaticCacheOnLoadFromDataset(dataset),
|
|
86
|
+
autoVisible: parseBooleanOption(dataset.formieAutoVisible, true),
|
|
87
|
+
compatibility: parseBooleanOption(dataset.formieCompatibility, false),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeMode(mode: FormMountOptions['mode'] | undefined): FormMode {
|
|
92
|
+
return mode || 'server-rendered';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeTransport(transport: FormMountOptions['transport'] | undefined): FormTransport {
|
|
96
|
+
return transport || 'rest';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getFormFromTarget(target: Element): HTMLFormElement | null {
|
|
100
|
+
if (target instanceof HTMLFormElement) {
|
|
101
|
+
return target;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return target.querySelector('form');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function warnCompatibilityOnce(key: string, message: string): void {
|
|
108
|
+
if (compatibilityWarnings.has(key)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
compatibilityWarnings.add(key);
|
|
113
|
+
debug.warn(message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveEndpointAgainstBase(endpoint: string, baseEndpoint?: string): string {
|
|
117
|
+
if (!endpoint) {
|
|
118
|
+
return endpoint;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Absolute endpoint stays untouched.
|
|
122
|
+
try {
|
|
123
|
+
return new URL(endpoint).toString();
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
// fall through
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!baseEndpoint) {
|
|
129
|
+
return endpoint;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
return new URL(endpoint, baseEndpoint).toString();
|
|
134
|
+
} catch (_error) {
|
|
135
|
+
return endpoint;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveHeadlessEndpoint(baseOrEndpoint: string | undefined, actionPath: string): string {
|
|
140
|
+
const candidate = (baseOrEndpoint || '').trim();
|
|
141
|
+
|
|
142
|
+
if (!candidate) {
|
|
143
|
+
return actionPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (candidate.includes('/actions/')) {
|
|
147
|
+
return candidate;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resolveEndpointAgainstBase(actionPath, candidate);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveHtmlRenderEndpoint(options: FormMountOptions, target: Element): string {
|
|
154
|
+
return resolveHeadlessEndpoint(options.endpoint || (target as HTMLElement).dataset.formieEndpoint, DEFAULT_HEADLESS_RENDER_ACTION);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveGraphqlEndpoint(options: FormMountOptions, target: Element): string {
|
|
158
|
+
const candidate = (options.endpoint || (target as HTMLElement).dataset.formieEndpoint || '').trim();
|
|
159
|
+
|
|
160
|
+
if (!candidate) {
|
|
161
|
+
return DEFAULT_HEADLESS_GRAPHQL_ENDPOINT;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (candidate.includes('/graphql') || candidate.endsWith('/api') || candidate.includes('/actions/graphql/')) {
|
|
165
|
+
return candidate;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return resolveEndpointAgainstBase(DEFAULT_HEADLESS_GRAPHQL_ENDPOINT, candidate);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveRefreshTokensEndpoint(options: FormMountOptions, target: Element): string {
|
|
172
|
+
return resolveHeadlessEndpoint(
|
|
173
|
+
(target as HTMLElement).dataset.formieRefreshTokensEndpoint || options.endpoint || (target as HTMLElement).dataset.formieEndpoint,
|
|
174
|
+
DEFAULT_HEADLESS_REFRESH_TOKENS_ACTION,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function mergeSearchParams(sourceUrl: string | null, destinationUrl: string): string {
|
|
179
|
+
if (!sourceUrl) {
|
|
180
|
+
return destinationUrl;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const source = new URL(sourceUrl, window.location.origin);
|
|
185
|
+
const destination = new URL(destinationUrl, window.location.origin);
|
|
186
|
+
|
|
187
|
+
source.searchParams.forEach((value, key) => {
|
|
188
|
+
if (!destination.searchParams.has(key)) {
|
|
189
|
+
destination.searchParams.set(key, value);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return destination.toString();
|
|
194
|
+
} catch (_error) {
|
|
195
|
+
return destinationUrl;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeHeadlessManagedUrls(target: Element, form: HTMLFormElement, options: FormMountOptions): void {
|
|
200
|
+
const baseEndpoint = options.endpoint || (target as HTMLElement).dataset.formieEndpoint;
|
|
201
|
+
const submitAction = resolveHeadlessEndpoint(baseEndpoint, DEFAULT_HEADLESS_SUBMIT_ACTION);
|
|
202
|
+
const existingAction = form.getAttribute('action');
|
|
203
|
+
|
|
204
|
+
form.setAttribute('action', mergeSearchParams(existingAction, submitAction));
|
|
205
|
+
|
|
206
|
+
form.querySelectorAll<HTMLAnchorElement>('[data-formie-tab-link]').forEach((link) => {
|
|
207
|
+
const existingHref = link.getAttribute('href');
|
|
208
|
+
const setPageEndpoint = resolveHeadlessEndpoint(baseEndpoint, DEFAULT_HEADLESS_SET_PAGE_ACTION);
|
|
209
|
+
|
|
210
|
+
link.setAttribute('href', mergeSearchParams(existingHref, setPageEndpoint));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
form.querySelectorAll<HTMLElement>('[data-formie-file-upload-hydrate-endpoint]').forEach((input) => {
|
|
214
|
+
input.setAttribute(
|
|
215
|
+
'data-formie-file-upload-hydrate-endpoint',
|
|
216
|
+
resolveHeadlessEndpoint(baseEndpoint, DEFAULT_FILE_UPLOAD_HYDRATE_ACTION),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ensureSupportedHeadlessTransport(transport: FormTransport, mode: FormMode): void {
|
|
222
|
+
if (transport === 'graphql' && mode !== 'server-rendered') {
|
|
223
|
+
throw new Error(`Formie ${mode} mode does not support GraphQL transport yet.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseBooleanDatasetValue(value: string | undefined): boolean {
|
|
228
|
+
if (value == null) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const normalized = value.trim().toLowerCase();
|
|
233
|
+
return normalized === 'true' || normalized === '1' || normalized === '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasAutomaticSubmissionState(form: HTMLFormElement): boolean {
|
|
237
|
+
return parseBooleanOption(form.dataset.formieAutomaticSubmissionState, true);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolveClearSubmissionEndpoint(options: FormMountOptions, target: Element, form: HTMLFormElement): string {
|
|
241
|
+
return resolveHeadlessEndpoint(
|
|
242
|
+
form.dataset.formieClearSubmissionEndpoint || options.endpoint || (target as HTMLElement).dataset.formieEndpoint,
|
|
243
|
+
DEFAULT_HEADLESS_CLEAR_SUBMISSION_ACTION,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function shouldEnableUnloadWarning(form: HTMLFormElement): boolean {
|
|
248
|
+
return parseBooleanDatasetValue(form.dataset.formieUnloadWarning);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function markInternalNavigation(form: HTMLFormElement, reason: string): void {
|
|
252
|
+
form.setAttribute('data-formie-internal-navigation', reason);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function clearInternalNavigation(form: HTMLFormElement): void {
|
|
256
|
+
form.removeAttribute('data-formie-internal-navigation');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function hasInternalNavigation(form: HTMLFormElement): boolean {
|
|
260
|
+
return form.getAttribute('data-formie-internal-navigation') !== null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function urlHasSearchParam(sourceUrl: string | null, param: string): boolean {
|
|
264
|
+
if (!sourceUrl) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
return new URL(sourceUrl, window.location.origin).searchParams.has(param);
|
|
270
|
+
} catch (_error) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formHasResumeTokenState(form: HTMLFormElement): boolean {
|
|
276
|
+
return urlHasSearchParam(window.location.href, 'resumeToken')
|
|
277
|
+
|| urlHasSearchParam(form.getAttribute('action'), 'resumeToken');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isSameTabClickEvent(event: Event): boolean {
|
|
281
|
+
if (!(event instanceof MouseEvent)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseIntegerDatasetValue(value: string | undefined, fallback = 0): number {
|
|
289
|
+
if (!value) {
|
|
290
|
+
return fallback;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const parsed = Number.parseInt(value, 10);
|
|
294
|
+
|
|
295
|
+
if (!Number.isFinite(parsed)) {
|
|
296
|
+
return fallback;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return parsed;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getSubmitDelayMs(form: HTMLFormElement): number {
|
|
303
|
+
return Math.max(0, parseIntegerDatasetValue(form.dataset.formieSubmitDelay, DEFAULT_SUBMIT_DELAY_MS));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function shouldValidateOnSubmit(form: HTMLFormElement): boolean {
|
|
307
|
+
return parseBooleanDatasetValue(form.dataset.formieValidationOnSubmit);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function waitForSubmitDelay(form: HTMLFormElement): Promise<void> {
|
|
311
|
+
const delay = getSubmitDelayMs(form);
|
|
312
|
+
|
|
313
|
+
if (delay < 1) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await new Promise((resolve) => {
|
|
318
|
+
window.setTimeout(resolve, delay);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseJsonAttribute<T>(element: Element | null, attributeName: string): T | null {
|
|
323
|
+
const rawValue = element?.getAttribute(attributeName)?.trim();
|
|
324
|
+
|
|
325
|
+
if (!rawValue) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(rawValue) as T;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`[formie] Failed to parse ${attributeName}.`, error);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getEmbeddedPayload(target: Element, form: HTMLFormElement | null): FormEndpointPayload | null {
|
|
338
|
+
const payloadRoot = form || (target instanceof HTMLFormElement ? target : null);
|
|
339
|
+
|
|
340
|
+
if (!payloadRoot) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Server-rendered forms carry the same minimal payload shape that endpoint-
|
|
345
|
+
// rendered forms receive, which keeps mount behavior identical in both flows.
|
|
346
|
+
const modules = parseJsonAttribute<FormEndpointPayload['modules']>(payloadRoot, 'data-formie-modules');
|
|
347
|
+
const theme = parseJsonAttribute<FormEndpointPayload['theme']>(payloadRoot, 'data-formie-theme');
|
|
348
|
+
|
|
349
|
+
if (!modules && !theme) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
modules: modules || undefined,
|
|
355
|
+
theme: theme || undefined,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isElementVisible(target: Element): boolean {
|
|
360
|
+
if (!(target instanceof HTMLElement)) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!target.isConnected) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (target.hidden || target.closest('[hidden]')) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const style = window.getComputedStyle(target);
|
|
373
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return target.getClientRects().length > 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isWithinScope(target: Element, scope: ParentNode): boolean {
|
|
381
|
+
if (scope === document) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (scope instanceof Element) {
|
|
386
|
+
return scope === target || scope.contains(target);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getTargetDebugLabel(target: Element): string {
|
|
393
|
+
const element = target as HTMLElement;
|
|
394
|
+
const id = element.id ? `#${element.id}` : '';
|
|
395
|
+
const handle = element.dataset?.formieHandle ? `[handle="${element.dataset.formieHandle}"]` : '';
|
|
396
|
+
const tag = element.tagName ? element.tagName.toLowerCase() : 'element';
|
|
397
|
+
return `${tag}${id}${handle}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function applyRefreshTokensToForm(form: HTMLFormElement, refreshTokens: FormEndpointPayload['refreshTokens'] | null | undefined): void {
|
|
401
|
+
if (!refreshTokens) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Refresh-tokens mutates continuity inputs in place so cached or long-lived
|
|
406
|
+
// forms keep submitting with fresh CSRF/request/captcha state.
|
|
407
|
+
if (refreshTokens.csrf?.param && refreshTokens.csrf?.token) {
|
|
408
|
+
const csrfInput = form.querySelector(`input[name="${refreshTokens.csrf.param}"]`) as HTMLInputElement | null;
|
|
409
|
+
if (csrfInput) {
|
|
410
|
+
csrfInput.value = refreshTokens.csrf.token;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (refreshTokens.requestToken) {
|
|
415
|
+
const requestTokenInput = form.querySelector('input[name="requestToken"]') as HTMLInputElement | null;
|
|
416
|
+
if (requestTokenInput) {
|
|
417
|
+
requestTokenInput.value = refreshTokens.requestToken;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (refreshTokens.renderId) {
|
|
422
|
+
const renderIdInput = form.querySelector('input[name="renderId"]') as HTMLInputElement | null;
|
|
423
|
+
if (renderIdInput) {
|
|
424
|
+
renderIdInput.value = refreshTokens.renderId;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (refreshTokens.captchas && typeof refreshTokens.captchas === 'object') {
|
|
429
|
+
Object.values(refreshTokens.captchas).forEach((captchaEntry) => {
|
|
430
|
+
if (!captchaEntry || typeof captchaEntry !== 'object') {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const entry = captchaEntry as { sessionKey?: string; value?: string };
|
|
435
|
+
if (!entry.sessionKey) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const captchaInput = form.querySelector(`input[name="${entry.sessionKey}"]`) as HTMLInputElement | null;
|
|
440
|
+
if (captchaInput && typeof entry.value === 'string') {
|
|
441
|
+
captchaInput.value = entry.value;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function ensureHtmlRender(target: Element, options: FormMountOptions): Promise<FormEndpointPayload | null> {
|
|
448
|
+
const mode = normalizeMode(options.mode);
|
|
449
|
+
const transport = normalizeTransport(options.transport);
|
|
450
|
+
|
|
451
|
+
if (mode !== 'server-rendered') {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (options.payload) {
|
|
456
|
+
if (options.payload.html) {
|
|
457
|
+
(target as HTMLElement).innerHTML = options.payload.html;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return options.payload;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
ensureSupportedHeadlessTransport(transport, mode);
|
|
464
|
+
|
|
465
|
+
const hasForm = !!getFormFromTarget(target);
|
|
466
|
+
const formHandle = options.formHandle || (target as HTMLElement).dataset.formieHandle;
|
|
467
|
+
|
|
468
|
+
// Server-rendered forms only render here when the host surface is just a mount shell.
|
|
469
|
+
// Pre-rendered forms skip this and continue through the normal mount path.
|
|
470
|
+
if (hasForm || !formHandle) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const renderOptions = {
|
|
475
|
+
mode,
|
|
476
|
+
endpoint: options.endpoint,
|
|
477
|
+
locale: options.locale,
|
|
478
|
+
siteId: options.siteId,
|
|
479
|
+
theme: options.theme,
|
|
480
|
+
themeConfig: options.themeConfig,
|
|
481
|
+
};
|
|
482
|
+
const endpoint = transport === 'graphql'
|
|
483
|
+
? resolveGraphqlEndpoint(options, target)
|
|
484
|
+
: resolveHtmlRenderEndpoint(options, target);
|
|
485
|
+
const payload = transport === 'graphql'
|
|
486
|
+
? await requestGraphqlRender(endpoint, formHandle, renderOptions)
|
|
487
|
+
: await requestRender(endpoint, formHandle, {
|
|
488
|
+
...renderOptions,
|
|
489
|
+
endpoint,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (payload?.html) {
|
|
493
|
+
(target as HTMLElement).innerHTML = payload.html;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return payload;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function refreshTokensAfterSubmitIfNeeded(target: Element, options: FormMountOptions, form: HTMLFormElement): Promise<void> {
|
|
500
|
+
if (options.refreshTokens === false) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
ensureSupportedHeadlessTransport(normalizeTransport(options.transport), normalizeMode(options.mode));
|
|
505
|
+
|
|
506
|
+
const formHandle = options.formHandle || (target as HTMLElement).dataset.formieHandle;
|
|
507
|
+
if (!formHandle) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Refresh-tokens happens after most submit outcomes so subsequent page moves,
|
|
512
|
+
// retries, or repeated submits reuse fresh request/captcha tokens.
|
|
513
|
+
const endpoint = resolveRefreshTokensEndpoint(options, target);
|
|
514
|
+
const renderIdInput = form.querySelector('input[name="renderId"]') as HTMLInputElement | null;
|
|
515
|
+
const renderId = renderIdInput?.value || undefined;
|
|
516
|
+
const refreshTokens = await requestRefreshTokens(endpoint, formHandle, renderId);
|
|
517
|
+
applyRefreshTokensToForm(form, refreshTokens);
|
|
518
|
+
dispatchFormieDomEvent(target, 'formie:refresh-tokens:refreshed', refreshTokens);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function bindFormEvents(
|
|
522
|
+
target: Element,
|
|
523
|
+
form: HTMLFormElement,
|
|
524
|
+
options: FormMountOptions,
|
|
525
|
+
bus: EventBus,
|
|
526
|
+
validator: FormieValidator | null,
|
|
527
|
+
unbinds: Array<() => void>,
|
|
528
|
+
): void {
|
|
529
|
+
const submitMethod = String(
|
|
530
|
+
form.dataset.formieSubmitMethod || '',
|
|
531
|
+
).trim().toLowerCase();
|
|
532
|
+
const clearSubmissionEndpoint = resolveClearSubmissionEndpoint(options, target, form);
|
|
533
|
+
let allowNativeSubmit = false;
|
|
534
|
+
const submitButtons = form.querySelectorAll('[data-formie-action]');
|
|
535
|
+
const setPendingAction = (action: FormAction | null) => {
|
|
536
|
+
if (action) {
|
|
537
|
+
// Store the last explicit button action so keyboard submits and synthetic
|
|
538
|
+
// submits still preserve back/save intent when the submitter is absent.
|
|
539
|
+
form.setAttribute('data-formie-pending-action', action);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
form.removeAttribute('data-formie-pending-action');
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (shouldEnableUnloadWarning(form)) {
|
|
547
|
+
const unloadWarning = createFormUnloadWarningGuard(form, {
|
|
548
|
+
shouldWarn: () => {
|
|
549
|
+
return !hasInternalNavigation(form);
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
const handleSubmitResult = (event: Event): void => {
|
|
553
|
+
if (!(event instanceof CustomEvent)) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const result = event.detail as FormSubmitResult | null;
|
|
558
|
+
|
|
559
|
+
if (!result?.ok) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (result.action === 'save') {
|
|
564
|
+
unloadWarning.scheduleBaselineCapture();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const handleStateReset = (): void => {
|
|
568
|
+
unloadWarning.scheduleBaselineCapture();
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
target.addEventListener('formie:submit:result', handleSubmitResult as EventListener);
|
|
572
|
+
form.addEventListener('formie:state:reset', handleStateReset as EventListener);
|
|
573
|
+
unbinds.push(() => {
|
|
574
|
+
target.removeEventListener('formie:submit:result', handleSubmitResult as EventListener);
|
|
575
|
+
form.removeEventListener('formie:state:reset', handleStateReset as EventListener);
|
|
576
|
+
unloadWarning.destroy();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
submitButtons.forEach((button) => {
|
|
581
|
+
const handler = (event: Event) => {
|
|
582
|
+
const action = (event.currentTarget as HTMLElement).getAttribute('data-formie-action') as FormAction | null;
|
|
583
|
+
const submitAction = form.querySelector('input[name="submitAction"]') as HTMLInputElement | null;
|
|
584
|
+
|
|
585
|
+
setPendingAction(action);
|
|
586
|
+
|
|
587
|
+
if (action && submitAction) {
|
|
588
|
+
submitAction.value = action;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
button.addEventListener('click', handler);
|
|
593
|
+
unbinds.push(() => {
|
|
594
|
+
button.removeEventListener('click', handler);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const pageTabLinks = form.querySelectorAll('[data-formie-tab-link]');
|
|
599
|
+
|
|
600
|
+
pageTabLinks.forEach((link) => {
|
|
601
|
+
const handler = async(event: Event) => {
|
|
602
|
+
if (submitMethod !== 'ajax') {
|
|
603
|
+
if (isSameTabClickEvent(event)) {
|
|
604
|
+
markInternalNavigation(form, 'set-page');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Ajax multipage navigation updates UI immediately, then persists page
|
|
611
|
+
// continuity in the background so refresh/recovery stays in sync.
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
|
|
614
|
+
const currentTarget = event.currentTarget as HTMLAnchorElement | null;
|
|
615
|
+
const nextPageId = currentTarget?.getAttribute('data-formie-page-id');
|
|
616
|
+
const href = currentTarget?.getAttribute('href');
|
|
617
|
+
|
|
618
|
+
if (!nextPageId || !href) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
applyPageState(form, nextPageId);
|
|
623
|
+
dispatchFormieDomEvent(target, 'formie:page:navigate', {
|
|
624
|
+
pageId: nextPageId,
|
|
625
|
+
href,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const response = await requestSetPage(href, form, nextPageId);
|
|
630
|
+
|
|
631
|
+
dispatchFormieDomEvent(target, 'formie:page:navigate:after', {
|
|
632
|
+
pageId: nextPageId,
|
|
633
|
+
href,
|
|
634
|
+
response,
|
|
635
|
+
});
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error('[formie] Failed to persist page navigation state.', error);
|
|
638
|
+
dispatchFormieDomEvent(target, 'formie:page:navigate:error', {
|
|
639
|
+
pageId: nextPageId,
|
|
640
|
+
href,
|
|
641
|
+
error,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
link.addEventListener('click', handler);
|
|
647
|
+
unbinds.push(() => {
|
|
648
|
+
link.removeEventListener('click', handler);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
if (!hasAutomaticSubmissionState(form)) {
|
|
653
|
+
let requestedClearOnLeave = false;
|
|
654
|
+
const leaveHandler = () => {
|
|
655
|
+
if (requestedClearOnLeave || hasInternalNavigation(form) || formHasResumeTokenState(form)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
requestedClearOnLeave = true;
|
|
660
|
+
clearSubmissionOnUnload(clearSubmissionEndpoint, form);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
window.addEventListener('pagehide', leaveHandler);
|
|
664
|
+
window.addEventListener('beforeunload', leaveHandler);
|
|
665
|
+
unbinds.push(() => {
|
|
666
|
+
window.removeEventListener('pagehide', leaveHandler);
|
|
667
|
+
window.removeEventListener('beforeunload', leaveHandler);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const submitHandler = async(event: Event) => {
|
|
672
|
+
if (allowNativeSubmit) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const isAjaxSubmit = submitMethod === 'ajax';
|
|
677
|
+
if (!isAjaxSubmit) {
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
} else {
|
|
680
|
+
event.preventDefault();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Loading state guards the whole submit pipeline, including async module
|
|
684
|
+
// hooks, so duplicate native submits cannot race each other.
|
|
685
|
+
if (form.getAttribute('data-formie-loading') === 'true') {
|
|
686
|
+
const isInternalResubmit = form.getAttribute('data-formie-internal-resubmit') === 'true';
|
|
687
|
+
|
|
688
|
+
if (!isInternalResubmit) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
form.removeAttribute('data-formie-internal-resubmit');
|
|
693
|
+
} else {
|
|
694
|
+
form.removeAttribute('data-formie-internal-resubmit');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const submitEvent = event as SubmitEvent;
|
|
698
|
+
const submitter = submitEvent.submitter as HTMLElement | null;
|
|
699
|
+
const actionFromSubmitter = submitter?.getAttribute('data-formie-action') as FormAction | null;
|
|
700
|
+
const pendingAction = form.getAttribute('data-formie-pending-action') as FormAction | null;
|
|
701
|
+
|
|
702
|
+
const submitAction = form.querySelector('input[name="submitAction"]') as HTMLInputElement | null;
|
|
703
|
+
const action = actionFromSubmitter || pendingAction || (submitAction?.value as FormAction) || 'submit';
|
|
704
|
+
let result: FormSubmitResult | null = null;
|
|
705
|
+
let nativeSubmitStarted = false;
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
if (isAjaxSubmit) {
|
|
709
|
+
result = await executeAjaxSubmitFlow({
|
|
710
|
+
target,
|
|
711
|
+
form,
|
|
712
|
+
bus,
|
|
713
|
+
validator,
|
|
714
|
+
validateOnSubmit: shouldValidateOnSubmit(form),
|
|
715
|
+
action,
|
|
716
|
+
submitter,
|
|
717
|
+
waitForSubmitDelay,
|
|
718
|
+
onRefreshTokensAfterSubmit: async() => {
|
|
719
|
+
await refreshTokensAfterSubmitIfNeeded(target, options, form);
|
|
720
|
+
},
|
|
721
|
+
dispatchSubmitResult: (submitResult) => {
|
|
722
|
+
dispatchFormieDomEvent(target, 'formie:submit:result', submitResult);
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
clearSubmitFeedback(form);
|
|
727
|
+
setSubmitLoading(form, submitter);
|
|
728
|
+
await waitForSubmitDelay(form);
|
|
729
|
+
result = await runSubmitPipeline(form, action, bus, {
|
|
730
|
+
validator,
|
|
731
|
+
validateOnSubmit: shouldValidateOnSubmit(form),
|
|
732
|
+
preflightOnly: true,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
if (result.ok) {
|
|
736
|
+
dispatchPageClientEventForSubmit(form, action);
|
|
737
|
+
allowNativeSubmit = true;
|
|
738
|
+
markInternalNavigation(form, 'submit');
|
|
739
|
+
setPendingAction(null);
|
|
740
|
+
|
|
741
|
+
let nativeValidationFailed = false;
|
|
742
|
+
const nativeInvalidHandler = () => {
|
|
743
|
+
nativeValidationFailed = true;
|
|
744
|
+
allowNativeSubmit = false;
|
|
745
|
+
clearInternalNavigation(form);
|
|
746
|
+
clearSubmitLoading(form);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
if (typeof form.requestSubmit === 'function') {
|
|
750
|
+
// Keep the existing loading/disabled state intact while the
|
|
751
|
+
// browser performs the final native validation and navigation.
|
|
752
|
+
// The hidden submitAction already carries the clicked action.
|
|
753
|
+
form.addEventListener('invalid', nativeInvalidHandler, true);
|
|
754
|
+
try {
|
|
755
|
+
form.requestSubmit();
|
|
756
|
+
} finally {
|
|
757
|
+
form.removeEventListener('invalid', nativeInvalidHandler, true);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
form.submit();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (nativeValidationFailed) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
nativeSubmitStarted = true;
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
applySubmitResultUi(form, result);
|
|
772
|
+
dispatchFormieDomEvent(target, 'formie:submit:result', result);
|
|
773
|
+
clearInternalNavigation(form);
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
allowNativeSubmit = false;
|
|
777
|
+
result = {
|
|
778
|
+
ok: false,
|
|
779
|
+
code: 'SUBMIT_ERROR',
|
|
780
|
+
message: error instanceof Error ? error.message : 'Submission failed.',
|
|
781
|
+
formErrors: [error instanceof Error ? error.message : 'Submission failed.'],
|
|
782
|
+
};
|
|
783
|
+
applySubmitResultUi(form, result);
|
|
784
|
+
dispatchFormieDomEvent(target, 'formie:submit:result', result);
|
|
785
|
+
clearInternalNavigation(form);
|
|
786
|
+
} finally {
|
|
787
|
+
setPendingAction(null);
|
|
788
|
+
if (!isAjaxSubmit && !nativeSubmitStarted && !shouldKeepSubmitLoading(result)) {
|
|
789
|
+
clearSubmitLoading(form);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
form.addEventListener('submit', submitHandler);
|
|
795
|
+
unbinds.push(() => {
|
|
796
|
+
form.removeEventListener('submit', submitHandler);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function refreshTokensIfNeeded(target: Element, options: FormMountOptions, form: HTMLFormElement | null): Promise<void> {
|
|
801
|
+
if (options.refreshTokens === false) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!options.staticCache) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
ensureSupportedHeadlessTransport(normalizeTransport(options.transport), normalizeMode(options.mode));
|
|
810
|
+
|
|
811
|
+
const formHandle = options.formHandle || (target as HTMLElement).dataset.formieHandle;
|
|
812
|
+
const endpoint = resolveRefreshTokensEndpoint(options, target);
|
|
813
|
+
const renderIdInput = form?.querySelector('input[name="renderId"]') as HTMLInputElement | null;
|
|
814
|
+
const renderId = renderIdInput?.value || undefined;
|
|
815
|
+
|
|
816
|
+
if (!formHandle) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Refresh-tokens-before-ready is the cache-safe path: forms can render from SSR/cache
|
|
821
|
+
// and still receive fresh transport tokens before the user submits anything.
|
|
822
|
+
const refreshTokens = await requestRefreshTokens(endpoint, formHandle, renderId);
|
|
823
|
+
|
|
824
|
+
if (!refreshTokens || !form) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
applyRefreshTokensToForm(form, refreshTokens);
|
|
829
|
+
|
|
830
|
+
dispatchFormieDomEvent(target, 'formie:refresh-tokens:after', refreshTokens);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function createFormieClient(): FormieClient {
|
|
834
|
+
const instances = new Map<Element, InternalInstanceState>();
|
|
835
|
+
const moduleRegistry = new ModuleRegistry();
|
|
836
|
+
const pendingVisibilityMounts = new Map<Element, () => void>();
|
|
837
|
+
const pendingUnmounts = new Map<Element, Promise<void>>();
|
|
838
|
+
const stageNames: Array<'prepare' | 'normalize' | 'validate' | 'screen' | 'authorize' | 'dispatch' | 'finalize'> = [
|
|
839
|
+
'prepare',
|
|
840
|
+
'normalize',
|
|
841
|
+
'validate',
|
|
842
|
+
'screen',
|
|
843
|
+
'authorize',
|
|
844
|
+
'dispatch',
|
|
845
|
+
'finalize',
|
|
846
|
+
];
|
|
847
|
+
|
|
848
|
+
const unmount = async(target: Element): Promise<void> => {
|
|
849
|
+
const inFlightUnmount = pendingUnmounts.get(target);
|
|
850
|
+
if (inFlightUnmount) {
|
|
851
|
+
await inFlightUnmount;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const unmountPromise = (async() => {
|
|
856
|
+
debug.log('Unmount requested.', { target: getTargetDebugLabel(target) });
|
|
857
|
+
const pendingUnmount = pendingVisibilityMounts.get(target);
|
|
858
|
+
if (pendingUnmount) {
|
|
859
|
+
pendingUnmount();
|
|
860
|
+
pendingVisibilityMounts.delete(target);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const state = instances.get(target);
|
|
864
|
+
|
|
865
|
+
if (!state) {
|
|
866
|
+
debug.log('Unmount skipped (no mounted state).', { target: getTargetDebugLabel(target) });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
dispatchFormieDomEvent(target, 'formie:unmount:before', {
|
|
871
|
+
id: state.instance.id,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
state.unbinds.forEach((unbind) => {
|
|
875
|
+
unbind();
|
|
876
|
+
});
|
|
877
|
+
state.unbinds = [];
|
|
878
|
+
|
|
879
|
+
state.validator?.destroy();
|
|
880
|
+
state.validator = null;
|
|
881
|
+
|
|
882
|
+
for (const moduleInstance of state.modules) {
|
|
883
|
+
await moduleInstance.destroy();
|
|
884
|
+
}
|
|
885
|
+
state.modules = [];
|
|
886
|
+
|
|
887
|
+
state.bus.clear();
|
|
888
|
+
instances.delete(target);
|
|
889
|
+
|
|
890
|
+
dispatchFormieDomEvent(target, 'formie:unmount:after', {
|
|
891
|
+
id: state.instance.id,
|
|
892
|
+
});
|
|
893
|
+
debug.log('Unmount complete.', { id: state.instance.id, target: getTargetDebugLabel(target) });
|
|
894
|
+
})().finally(() => {
|
|
895
|
+
pendingUnmounts.delete(target);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
pendingUnmounts.set(target, unmountPromise);
|
|
899
|
+
await unmountPromise;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const mount = async(target: Element, options: FormMountOptions): Promise<FormieFormInstance> => {
|
|
903
|
+
debug.log('Mount requested.', {
|
|
904
|
+
target: getTargetDebugLabel(target),
|
|
905
|
+
mode: options.mode,
|
|
906
|
+
autoVisible: options.autoVisible,
|
|
907
|
+
});
|
|
908
|
+
const pendingMount = pendingVisibilityMounts.get(target);
|
|
909
|
+
if (pendingMount) {
|
|
910
|
+
pendingMount();
|
|
911
|
+
pendingVisibilityMounts.delete(target);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const existing = instances.get(target);
|
|
915
|
+
|
|
916
|
+
if (existing) {
|
|
917
|
+
debug.log('Mount skipped (already mounted).', {
|
|
918
|
+
id: existing.instance.id,
|
|
919
|
+
target: getTargetDebugLabel(target),
|
|
920
|
+
});
|
|
921
|
+
return existing.instance;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const bus = new EventBus();
|
|
925
|
+
const unbinds: Array<() => void> = [];
|
|
926
|
+
const id = (target as HTMLElement)?.id || `formie-${instances.size + 1}`;
|
|
927
|
+
const mergedFromDom = inferOptionsFromElement(target);
|
|
928
|
+
const normalizedOptions: FormMountOptions = {
|
|
929
|
+
...mergedFromDom,
|
|
930
|
+
...options,
|
|
931
|
+
mode: normalizeMode(options.mode ?? mergedFromDom.mode),
|
|
932
|
+
transport: normalizeTransport(options.transport ?? mergedFromDom.transport),
|
|
933
|
+
};
|
|
934
|
+
const compatibilityOptions = resolveLegacyCompatibilityOptions(normalizedOptions.compatibility);
|
|
935
|
+
|
|
936
|
+
if (normalizedOptions.mode !== 'server-rendered' && !getFormFromTarget(target)) {
|
|
937
|
+
throw new Error(`Formie ${normalizedOptions.mode} mode is not implemented yet in the browser client.`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const renderPayload = await ensureHtmlRender(target, normalizedOptions);
|
|
941
|
+
const form = getFormFromTarget(target);
|
|
942
|
+
normalizedOptions.staticCache =
|
|
943
|
+
options.staticCache ??
|
|
944
|
+
(form
|
|
945
|
+
? inferStaticCacheOnLoadFromDataset(form.dataset)
|
|
946
|
+
: inferStaticCacheOnLoadFromDataset((target as HTMLElement).dataset));
|
|
947
|
+
const embeddedPayload = getEmbeddedPayload(target, form);
|
|
948
|
+
const payload = renderPayload || embeddedPayload
|
|
949
|
+
? {
|
|
950
|
+
...(renderPayload || {}),
|
|
951
|
+
...(embeddedPayload || {}),
|
|
952
|
+
}
|
|
953
|
+
: null;
|
|
954
|
+
const themeClassMap = payload?.theme as ThemeClassMap | undefined;
|
|
955
|
+
const stateStore: Record<string, unknown> = {};
|
|
956
|
+
const moduleManifest = ((payload?.modules || []) as FormModuleManifest[]).filter((item) => {
|
|
957
|
+
return !!item?.id && !!item?.type;
|
|
958
|
+
});
|
|
959
|
+
debug.log('Resolved mount payload.', {
|
|
960
|
+
target: getTargetDebugLabel(target),
|
|
961
|
+
hasRenderPayload: !!renderPayload,
|
|
962
|
+
hasEmbeddedPayload: !!embeddedPayload,
|
|
963
|
+
moduleCount: moduleManifest.length,
|
|
964
|
+
});
|
|
965
|
+
const resolvedThemeClassMap = registerThemeClassMap(target, themeClassMap, form);
|
|
966
|
+
|
|
967
|
+
const validator = form ? new FormieValidator(form, {
|
|
968
|
+
live: parseBooleanDatasetValue(form.dataset.formieValidationOnFocus),
|
|
969
|
+
errorMessage: form.dataset.formieErrorMessage || '',
|
|
970
|
+
fieldContainerErrorClass: resolvedThemeClassMap.fieldLayoutError || [],
|
|
971
|
+
inputErrorClass: resolvedThemeClassMap.fieldControlError || [],
|
|
972
|
+
messagesClass: resolvedThemeClassMap.fieldErrors || [],
|
|
973
|
+
messageClass: resolvedThemeClassMap.fieldError || [],
|
|
974
|
+
}) : null;
|
|
975
|
+
|
|
976
|
+
if (form && validator) {
|
|
977
|
+
const formWithValidationApi = form as FormWithValidationApi;
|
|
978
|
+
formWithValidationApi.formieValidation = validator;
|
|
979
|
+
// Preserve the validator on the form element for browser helpers that
|
|
980
|
+
// only receive DOM references during later submit/result transitions.
|
|
981
|
+
stateStore.validation = validator;
|
|
982
|
+
|
|
983
|
+
const validatorDetail = {
|
|
984
|
+
validator,
|
|
985
|
+
addValidator: validator.addValidator.bind(validator),
|
|
986
|
+
removeValidator: validator.removeValidator.bind(validator),
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
dispatchFormieDomEvent(form, 'formie:validator:ready', validatorDetail);
|
|
990
|
+
dispatchFormieDomEvent(target, 'formie:validator:ready', validatorDetail);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (form) {
|
|
994
|
+
if (renderPayload || normalizedOptions.endpoint || (target as HTMLElement).dataset.formieEndpoint) {
|
|
995
|
+
normalizeHeadlessManagedUrls(target, form, normalizedOptions);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
syncPageTabErrors(form);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (Object.keys(resolvedThemeClassMap).length) {
|
|
1002
|
+
dispatchFormieDomEvent(target, 'formie:theme:applied', {
|
|
1003
|
+
hasClasses: true,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Modules run against the mounted DOM surface, not against server payload
|
|
1008
|
+
// objects, so the loader resolves real targets before calling setup().
|
|
1009
|
+
const modules = await loadModulesFromManifest(moduleManifest, {
|
|
1010
|
+
registry: moduleRegistry,
|
|
1011
|
+
matchContext: {
|
|
1012
|
+
root: target,
|
|
1013
|
+
form,
|
|
1014
|
+
mode: normalizedOptions.mode,
|
|
1015
|
+
},
|
|
1016
|
+
setupContext: {
|
|
1017
|
+
formId: id,
|
|
1018
|
+
root: target,
|
|
1019
|
+
form,
|
|
1020
|
+
target,
|
|
1021
|
+
scope: 'form',
|
|
1022
|
+
state: stateStore,
|
|
1023
|
+
on: (eventName, callback) => {
|
|
1024
|
+
return bus.on(eventName, callback);
|
|
1025
|
+
},
|
|
1026
|
+
emit: (eventName, payload) => {
|
|
1027
|
+
dispatchFormieDomEvent(target, eventName, payload);
|
|
1028
|
+
return bus.emitSafe(eventName, payload).then((emitReport) => {
|
|
1029
|
+
if (emitReport.failed.length > 0) {
|
|
1030
|
+
debug.warn('Lifecycle listeners failed.', {
|
|
1031
|
+
eventName,
|
|
1032
|
+
failed: emitReport.failed.length,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
debug.log('Module setup complete.', {
|
|
1040
|
+
target: getTargetDebugLabel(target),
|
|
1041
|
+
moduleInstances: modules.length,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const instance: FormieFormInstance = {
|
|
1045
|
+
id,
|
|
1046
|
+
root: target,
|
|
1047
|
+
submit: async(action = 'submit') => {
|
|
1048
|
+
debug.log('Submit requested.', {
|
|
1049
|
+
id,
|
|
1050
|
+
target: getTargetDebugLabel(target),
|
|
1051
|
+
action,
|
|
1052
|
+
});
|
|
1053
|
+
if (!form) {
|
|
1054
|
+
return {
|
|
1055
|
+
ok: false,
|
|
1056
|
+
code: 'FORM_NOT_FOUND',
|
|
1057
|
+
message: 'No form element found for mount target.',
|
|
1058
|
+
formErrors: ['No form element found for mount target.'],
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const submitAction = form.querySelector('input[name="submitAction"]') as HTMLInputElement | null;
|
|
1063
|
+
|
|
1064
|
+
if (submitAction) {
|
|
1065
|
+
submitAction.value = action;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (form.getAttribute('data-formie-loading') === 'true') {
|
|
1069
|
+
return {
|
|
1070
|
+
ok: false,
|
|
1071
|
+
code: 'SUBMIT_IN_PROGRESS',
|
|
1072
|
+
message: 'Submission already in progress.',
|
|
1073
|
+
formErrors: [],
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const fallbackSubmitter = form.querySelector(`[data-formie-action="${action}"]`) as HTMLElement | null;
|
|
1078
|
+
const result = await executeAjaxSubmitFlow({
|
|
1079
|
+
id,
|
|
1080
|
+
target,
|
|
1081
|
+
form,
|
|
1082
|
+
bus,
|
|
1083
|
+
validator,
|
|
1084
|
+
validateOnSubmit: shouldValidateOnSubmit(form),
|
|
1085
|
+
action,
|
|
1086
|
+
submitter: fallbackSubmitter,
|
|
1087
|
+
waitForSubmitDelay,
|
|
1088
|
+
onRefreshTokensAfterSubmit: async() => {
|
|
1089
|
+
await refreshTokensAfterSubmitIfNeeded(target, normalizedOptions, form);
|
|
1090
|
+
},
|
|
1091
|
+
dispatchSubmitResult: (submitResult) => {
|
|
1092
|
+
dispatchFormieDomEvent(target, 'formie:submit:result', submitResult);
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
debug.log('Submit completed.', {
|
|
1097
|
+
id,
|
|
1098
|
+
action,
|
|
1099
|
+
ok: result.ok,
|
|
1100
|
+
code: result.code,
|
|
1101
|
+
message: result.message,
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
return result;
|
|
1105
|
+
},
|
|
1106
|
+
destroy: async() => {
|
|
1107
|
+
await unmount(target);
|
|
1108
|
+
},
|
|
1109
|
+
on: (eventName, callback) => {
|
|
1110
|
+
return bus.on(eventName, callback);
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
if (form) {
|
|
1115
|
+
bindLegacyValidatorCompatibility({
|
|
1116
|
+
target,
|
|
1117
|
+
form,
|
|
1118
|
+
validatorDetail: validator ? {
|
|
1119
|
+
validator,
|
|
1120
|
+
addValidator: validator.addValidator.bind(validator),
|
|
1121
|
+
removeValidator: validator.removeValidator.bind(validator),
|
|
1122
|
+
} : null,
|
|
1123
|
+
options: compatibilityOptions,
|
|
1124
|
+
unbinds,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
bindLegacyDomEventCompatibility({
|
|
1128
|
+
target,
|
|
1129
|
+
form,
|
|
1130
|
+
instance,
|
|
1131
|
+
options: compatibilityOptions,
|
|
1132
|
+
unbinds,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (form) {
|
|
1137
|
+
bindFormEvents(target, form, normalizedOptions, bus, validator, unbinds);
|
|
1138
|
+
await refreshTokensIfNeeded(target, normalizedOptions, form);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
stageNames.forEach((stageName) => {
|
|
1142
|
+
// Stage fan-out keeps submit pipeline ownership centralized while still
|
|
1143
|
+
// letting modules participate before and after each stage.
|
|
1144
|
+
const beforeDomUnbind = bus.on(`formie:stage:${stageName}:before`, async(payload) => {
|
|
1145
|
+
dispatchFormieDomEvent(target, `formie:stage:${stageName}:before`, payload);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const beforeUnbind = bus.on(`formie:stage:${stageName}:before`, async(payload) => {
|
|
1149
|
+
for (const moduleInstance of modules) {
|
|
1150
|
+
if (moduleInstance.onBeforeStage) {
|
|
1151
|
+
await moduleInstance.onBeforeStage(payload as Parameters<NonNullable<FormieModuleInstance['onBeforeStage']>>[0]);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
const afterDomUnbind = bus.on(`formie:stage:${stageName}:after`, async(payload) => {
|
|
1157
|
+
dispatchFormieDomEvent(target, `formie:stage:${stageName}:after`, payload);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
const afterUnbind = bus.on(`formie:stage:${stageName}:after`, async(payload) => {
|
|
1161
|
+
const stagePayload = payload as {
|
|
1162
|
+
result?: FormSubmitResult;
|
|
1163
|
+
} & Parameters<NonNullable<FormieModuleInstance['onAfterStage']>>[0];
|
|
1164
|
+
|
|
1165
|
+
for (const moduleInstance of modules) {
|
|
1166
|
+
if (moduleInstance.onAfterStage) {
|
|
1167
|
+
await moduleInstance.onAfterStage(stagePayload, stagePayload.result);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
unbinds.push(beforeDomUnbind, beforeUnbind, afterDomUnbind, afterUnbind);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const submitBeforeUnbind = bus.on('formie:submit:before', async(payload) => {
|
|
1176
|
+
dispatchFormieDomEvent(target, 'formie:submit:before', payload);
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
const submitAfterUnbind = bus.on('formie:submit:after', async(payload) => {
|
|
1180
|
+
dispatchFormieDomEvent(target, 'formie:submit:after', payload);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const submitFinalBeforeUnbind = bus.on('formie:submit:final:before', async(payload) => {
|
|
1184
|
+
dispatchFormieDomEvent(target, 'formie:submit:final:before', payload);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
const submitFinalAfterUnbind = bus.on('formie:submit:final:after', async(payload) => {
|
|
1188
|
+
dispatchFormieDomEvent(target, 'formie:submit:final:after', payload);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
unbinds.push(
|
|
1192
|
+
submitBeforeUnbind,
|
|
1193
|
+
submitAfterUnbind,
|
|
1194
|
+
submitFinalBeforeUnbind,
|
|
1195
|
+
submitFinalAfterUnbind,
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
instances.set(target, {
|
|
1199
|
+
options: normalizedOptions,
|
|
1200
|
+
bus,
|
|
1201
|
+
form,
|
|
1202
|
+
validator,
|
|
1203
|
+
modules,
|
|
1204
|
+
unbinds,
|
|
1205
|
+
instance,
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
dispatchFormieDomEvent(target, 'formie:mount:after', {
|
|
1209
|
+
id,
|
|
1210
|
+
mode: normalizedOptions.mode as FormMode,
|
|
1211
|
+
});
|
|
1212
|
+
debug.log('Mount complete.', {
|
|
1213
|
+
id,
|
|
1214
|
+
target: getTargetDebugLabel(target),
|
|
1215
|
+
mode: normalizedOptions.mode,
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
return instance;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const mountWhenVisible = (target: Element, options: FormMountOptions): Promise<FormieFormInstance | null> => {
|
|
1222
|
+
if (!options.autoVisible || isElementVisible(target) || typeof IntersectionObserver === 'undefined') {
|
|
1223
|
+
return mount(target, options);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (instances.has(target)) {
|
|
1227
|
+
return Promise.resolve(instances.get(target)?.instance || null);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (pendingVisibilityMounts.has(target)) {
|
|
1231
|
+
debug.log('Mount deferred (already waiting visibility).', {
|
|
1232
|
+
target: getTargetDebugLabel(target),
|
|
1233
|
+
});
|
|
1234
|
+
return Promise.resolve(null);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Hidden/modal forms defer all setup until visible so heavy providers and
|
|
1238
|
+
// DOM bindings do not initialize against detached or zero-size surfaces.
|
|
1239
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1240
|
+
const hasVisibleEntry = entries.some((entry) => {
|
|
1241
|
+
return entry.target === target && entry.isIntersecting;
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
if (!hasVisibleEntry) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
observer.disconnect();
|
|
1249
|
+
pendingVisibilityMounts.delete(target);
|
|
1250
|
+
debug.log('Visibility reached, proceeding mount.', {
|
|
1251
|
+
target: getTargetDebugLabel(target),
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
void mount(target, {
|
|
1255
|
+
...options,
|
|
1256
|
+
autoVisible: false,
|
|
1257
|
+
});
|
|
1258
|
+
}, {
|
|
1259
|
+
threshold: 0.01,
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
observer.observe(target);
|
|
1263
|
+
pendingVisibilityMounts.set(target, () => {
|
|
1264
|
+
observer.disconnect();
|
|
1265
|
+
});
|
|
1266
|
+
debug.log('Mount deferred until visible.', {
|
|
1267
|
+
target: getTargetDebugLabel(target),
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
return Promise.resolve(null);
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const update = async(target: Element, options: Partial<FormMountOptions>): Promise<FormieFormInstance> => {
|
|
1274
|
+
const state = instances.get(target);
|
|
1275
|
+
|
|
1276
|
+
if (!state) {
|
|
1277
|
+
return mount(target, {
|
|
1278
|
+
...inferOptionsFromElement(target),
|
|
1279
|
+
...options,
|
|
1280
|
+
mode: options.mode || 'server-rendered',
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
state.options = {
|
|
1285
|
+
...state.options,
|
|
1286
|
+
...options,
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const themeClassMap = (
|
|
1290
|
+
options.payload?.theme
|
|
1291
|
+
|| state.options.payload?.theme
|
|
1292
|
+
|| getEmbeddedPayload(target, state.form)?.theme
|
|
1293
|
+
) as ThemeClassMap | undefined;
|
|
1294
|
+
const resolvedThemeClassMap = registerThemeClassMap(target, themeClassMap, state.form);
|
|
1295
|
+
|
|
1296
|
+
if (state.validator) {
|
|
1297
|
+
state.validator.config.fieldContainerErrorClass = resolvedThemeClassMap.fieldLayoutError || [];
|
|
1298
|
+
state.validator.config.inputErrorClass = resolvedThemeClassMap.fieldControlError || [];
|
|
1299
|
+
state.validator.config.messagesClass = resolvedThemeClassMap.fieldErrors || [];
|
|
1300
|
+
state.validator.config.messageClass = resolvedThemeClassMap.fieldError || [];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (Object.keys(resolvedThemeClassMap).length) {
|
|
1304
|
+
dispatchFormieDomEvent(target, 'formie:theme:applied', {
|
|
1305
|
+
hasClasses: true,
|
|
1306
|
+
reason: 'update',
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return state.instance;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const getInstance = (target: Element): FormieFormInstance | null => {
|
|
1314
|
+
return instances.get(target)?.instance || null;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const refreshForCache = async(targetOrId: Element | string): Promise<void> => {
|
|
1318
|
+
warnCompatibilityOnce(
|
|
1319
|
+
'refreshForCache',
|
|
1320
|
+
'Global `Formie.refreshForCache()` has been deprecated. Use built-in static-cache token refresh handling instead.',
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
let target: Element | null = null;
|
|
1324
|
+
|
|
1325
|
+
if (typeof targetOrId === 'string') {
|
|
1326
|
+
const byId = document.getElementById(targetOrId);
|
|
1327
|
+
|
|
1328
|
+
if (byId) {
|
|
1329
|
+
target = byId;
|
|
1330
|
+
} else {
|
|
1331
|
+
target = document.querySelector(`[data-formie-form-id="${targetOrId}"]`);
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
target = targetOrId;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (!target) {
|
|
1338
|
+
debug.warn('refreshForCache target not found.', {
|
|
1339
|
+
targetOrId,
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const state = instances.get(target);
|
|
1346
|
+
const form = getFormFromTarget(target);
|
|
1347
|
+
const options = state?.options || inferOptionsFromElement(target);
|
|
1348
|
+
|
|
1349
|
+
if (!form) {
|
|
1350
|
+
debug.warn('refreshForCache found no form element for target.', {
|
|
1351
|
+
target: getTargetDebugLabel(target),
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const formHandle = options.formHandle || (target as HTMLElement).dataset.formieHandle || form.dataset.formieHandle;
|
|
1358
|
+
const endpoint = resolveRefreshTokensEndpoint(options, target);
|
|
1359
|
+
const renderIdInput = form.querySelector('input[name="renderId"]') as HTMLInputElement | null;
|
|
1360
|
+
const renderId = renderIdInput?.value || undefined;
|
|
1361
|
+
|
|
1362
|
+
if (!formHandle) {
|
|
1363
|
+
debug.warn('refreshForCache found no form handle for target.', {
|
|
1364
|
+
target: getTargetDebugLabel(target),
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const refreshTokens = await requestRefreshTokens(endpoint, formHandle, renderId);
|
|
1371
|
+
|
|
1372
|
+
if (!refreshTokens) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
applyRefreshTokensToForm(form, refreshTokens);
|
|
1377
|
+
dispatchFormieDomEvent(target, 'formie:refresh-tokens:after', refreshTokens);
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
const registerModule = (
|
|
1381
|
+
moduleDefinition: FormieModuleDefinition,
|
|
1382
|
+
options?: Parameters<ModuleRegistry['register']>[1],
|
|
1383
|
+
): boolean => {
|
|
1384
|
+
return moduleRegistry.register(moduleDefinition, options);
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const unregisterModule = (moduleId: string): void => {
|
|
1388
|
+
moduleRegistry.unregister(moduleId);
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
const getRegisteredModules = (): FormieModuleDefinition[] => {
|
|
1392
|
+
return moduleRegistry.getAll();
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const scan = async(root?: ParentNode): Promise<FormieFormInstance[]> => {
|
|
1396
|
+
const scope = root || document;
|
|
1397
|
+
const targets = Array.from(scope.querySelectorAll(ROOT_SELECTORS));
|
|
1398
|
+
debug.log('Scan started.', {
|
|
1399
|
+
scope: scope === document ? 'document' : scope,
|
|
1400
|
+
targetCount: targets.length,
|
|
1401
|
+
});
|
|
1402
|
+
const results = await Promise.all(targets.map((target) => {
|
|
1403
|
+
const options = inferOptionsFromElement(target);
|
|
1404
|
+
return mountWhenVisible(target, options);
|
|
1405
|
+
}));
|
|
1406
|
+
const instances = results.filter((item): item is FormieFormInstance => !!item);
|
|
1407
|
+
debug.log('Scan finished.', {
|
|
1408
|
+
mountedCount: instances.length,
|
|
1409
|
+
deferredCount: targets.length - instances.length,
|
|
1410
|
+
});
|
|
1411
|
+
return instances;
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const observe = (root?: ParentNode): (() => void) => {
|
|
1415
|
+
if (typeof MutationObserver === 'undefined') {
|
|
1416
|
+
return () => {};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const scope = root || document;
|
|
1420
|
+
debug.log('Observer started.', {
|
|
1421
|
+
scope: scope === document ? 'document' : scope,
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
// Observe is the convenience layer for classic DOM-driven pages. It mounts
|
|
1425
|
+
// new roots and tears down removed ones so cache swaps and modal inserts do
|
|
1426
|
+
// not leak validators, event listeners, or module instances.
|
|
1427
|
+
const observer = new MutationObserver((mutations) => {
|
|
1428
|
+
mutations.forEach((mutation) => {
|
|
1429
|
+
mutation.addedNodes.forEach((node) => {
|
|
1430
|
+
if (!(node instanceof Element)) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (node.matches(ROOT_SELECTORS)) {
|
|
1435
|
+
debug.log('Observer detected new root.', {
|
|
1436
|
+
target: getTargetDebugLabel(node),
|
|
1437
|
+
});
|
|
1438
|
+
void mountWhenVisible(node, inferOptionsFromElement(node));
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
node.querySelectorAll(ROOT_SELECTORS).forEach((child) => {
|
|
1442
|
+
debug.log('Observer detected new nested root.', {
|
|
1443
|
+
target: getTargetDebugLabel(child),
|
|
1444
|
+
});
|
|
1445
|
+
void mountWhenVisible(child, inferOptionsFromElement(child));
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
mutation.removedNodes.forEach((node) => {
|
|
1450
|
+
if (!(node instanceof Element)) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (instances.has(node)) {
|
|
1455
|
+
debug.log('Observer detected removed root.', {
|
|
1456
|
+
target: getTargetDebugLabel(node),
|
|
1457
|
+
});
|
|
1458
|
+
void unmount(node);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
node.querySelectorAll(ROOT_SELECTORS).forEach((child) => {
|
|
1462
|
+
if (instances.has(child)) {
|
|
1463
|
+
debug.log('Observer detected removed nested root.', {
|
|
1464
|
+
target: getTargetDebugLabel(child),
|
|
1465
|
+
});
|
|
1466
|
+
void unmount(child);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
});
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
observer.observe(scope, {
|
|
1474
|
+
childList: true,
|
|
1475
|
+
subtree: true,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
return () => {
|
|
1479
|
+
observer.disconnect();
|
|
1480
|
+
debug.log('Observer stopped.');
|
|
1481
|
+
|
|
1482
|
+
pendingVisibilityMounts.forEach((cleanup, target) => {
|
|
1483
|
+
if (isWithinScope(target, scope)) {
|
|
1484
|
+
cleanup();
|
|
1485
|
+
pendingVisibilityMounts.delete(target);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
const roots: Element[] = [];
|
|
1490
|
+
|
|
1491
|
+
if (scope instanceof Element && scope.matches(ROOT_SELECTORS)) {
|
|
1492
|
+
roots.push(scope);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
scope.querySelectorAll(ROOT_SELECTORS).forEach((target) => {
|
|
1496
|
+
roots.push(target);
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
roots.forEach((target) => {
|
|
1500
|
+
if (instances.has(target)) {
|
|
1501
|
+
void unmount(target);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
};
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
return {
|
|
1508
|
+
mount,
|
|
1509
|
+
unmount,
|
|
1510
|
+
update,
|
|
1511
|
+
getInstance,
|
|
1512
|
+
refreshForCache,
|
|
1513
|
+
registerModule,
|
|
1514
|
+
unregisterModule,
|
|
1515
|
+
getRegisteredModules,
|
|
1516
|
+
scan,
|
|
1517
|
+
observe,
|
|
1518
|
+
};
|
|
1519
|
+
}
|