@verbb/formie-browser 1.0.4 → 1.0.5

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 (182) hide show
  1. package/CHANGELOG.md +62 -22
  2. package/dist/chunks/address-country-Rx6GQTND.js +68 -0
  3. package/dist/chunks/address-finder-yXU5yMjG.js +30 -0
  4. package/dist/chunks/address-state-BQ0xGV9n.js +344 -0
  5. package/dist/chunks/api-CmwLRq_n.js +780 -0
  6. package/dist/chunks/api-DMK8NSUI.js +407 -0
  7. package/dist/chunks/api-sPqGbOww.js +125 -0
  8. package/dist/chunks/async-nPFRNQ06.js +25 -0
  9. package/dist/chunks/bpoint-DSjqRv63.js +24 -0
  10. package/dist/chunks/calculations-CgVJcQ2P.js +299 -0
  11. package/dist/chunks/captcha-eu-CWJb64_j.js +31 -0
  12. package/dist/chunks/checkbox-radio-DQ0H67Tj.js +125 -0
  13. package/dist/chunks/chunk-3b4jIN3o.js +13 -0
  14. package/dist/chunks/combobox-D-2b42wn.js +1954 -0
  15. package/dist/chunks/conditions-CZ3FU5QO.js +412 -0
  16. package/dist/chunks/constants-DVcJAvc5.js +38 -0
  17. package/dist/chunks/country-from-ip-BwxKaBuJ.js +26 -0
  18. package/dist/chunks/custom-google-maps-BsaSirEZ.js +194 -0
  19. package/dist/chunks/custom-link-CJ1-FjEM.js +46 -0
  20. package/dist/chunks/custom-maps-DYW_pBid.js +161 -0
  21. package/dist/chunks/date-picker-5CA_-zGb.js +5311 -0
  22. package/dist/chunks/debug-BV0DvdHx.js +41 -0
  23. package/dist/chunks/dist-BF5GU3_9.js +2674 -0
  24. package/dist/chunks/event-names-BCI2FLD8.js +51 -0
  25. package/dist/chunks/eway-D3ctTbEk.js +70 -0
  26. package/dist/chunks/field-references.keys-58ZSTrCW.js +21 -0
  27. package/dist/chunks/field-references.resolver-CHwn0G0L.js +145 -0
  28. package/dist/chunks/file-upload-D49m8-DR.js +287 -0
  29. package/dist/chunks/friendly-captcha-v1-ByXzENYZ.js +31 -0
  30. package/dist/chunks/friendly-captcha-v2-BPdlmhQu.js +36 -0
  31. package/dist/chunks/friendly-challenge-8uPatWqk.js +832 -0
  32. package/dist/chunks/go-cardless-BwASSYSK.js +23 -0
  33. package/dist/chunks/google-address-OhKilPWE.js +161 -0
  34. package/dist/chunks/hcaptcha-CdCd9oEi.js +54 -0
  35. package/dist/chunks/hidden-D7_Ch-QN.js +26 -0
  36. package/dist/chunks/http-D-JExro7.js +27 -0
  37. package/dist/chunks/i18n-BY1ds1BL.js +121 -0
  38. package/dist/chunks/leaflet-src-j-lHNmhR.js +4585 -0
  39. package/dist/chunks/loqate-Cp2zvwpE.js +76 -0
  40. package/dist/chunks/mollie-COG-ko2D.js +23 -0
  41. package/dist/chunks/moneris-CMH8PlRc.js +117 -0
  42. package/dist/chunks/opayo-D2jsR5vh.js +113 -0
  43. package/dist/chunks/paddle-DxF88RfC.js +55 -0
  44. package/dist/chunks/paypal-b5pnwrA2.js +95 -0
  45. package/dist/chunks/payway-BJYu9DIR.js +55 -0
  46. package/dist/chunks/phone-country-D3ayr60L.js +2645 -0
  47. package/dist/chunks/place-kit-BGHklIZ-.js +48 -0
  48. package/dist/chunks/placekit-autocomplete.esm-DWVabQlq.js +1236 -0
  49. package/dist/chunks/recaptcha-enterprise-BSwi8IVX.js +65 -0
  50. package/dist/chunks/recaptcha-shared-B4zAescW.js +15 -0
  51. package/dist/chunks/recaptcha-v2-checkbox-CnDQmsKa.js +40 -0
  52. package/dist/chunks/recaptcha-v2-invisible-Cz5uIlp-.js +53 -0
  53. package/dist/chunks/recaptcha-v3-BtbbP111.js +27 -0
  54. package/dist/chunks/repeater-B5leVxZU.js +102 -0
  55. package/dist/chunks/rich-text-Cv8ADyk-.js +349 -0
  56. package/dist/chunks/scripts-6CYSUGKQ.js +24 -0
  57. package/dist/chunks/sdk-BhHAYgga.js +1397 -0
  58. package/dist/chunks/shared-Bx9s0i0P.js +100 -0
  59. package/dist/chunks/signature-CpgqHCEx.js +463 -0
  60. package/dist/chunks/snaptcha-vz1RnZ15.js +8 -0
  61. package/dist/chunks/square-BP8RWAKB.js +40 -0
  62. package/dist/chunks/stripe-MihN63uC.js +184 -0
  63. package/dist/chunks/styles-BfoIZwJp.js +19 -0
  64. package/dist/chunks/summary-D3AjxpYN.js +114 -0
  65. package/dist/chunks/table-C-lkQN6I.js +78 -0
  66. package/dist/chunks/text-limit-B09elPDB.js +125 -0
  67. package/dist/chunks/theme-classes-Tv7q7ToE.js +44 -0
  68. package/dist/chunks/turnstile-D_Pi1CKM.js +58 -0
  69. package/dist/chunks/utils-Vqp8i_RM.js +49401 -0
  70. package/dist/css/formie-theme.css +2 -0
  71. package/dist/css/theme/fields/_address-state.css +94 -0
  72. package/dist/css/theme/fields/_combobox.css +246 -0
  73. package/dist/css/theme/fields/_select.css +13 -0
  74. package/dist/index.js +2447 -39
  75. package/dist/js/core/create-formie-client.d.ts.map +1 -1
  76. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  77. package/dist/js/core/validation-focus.d.ts +4 -0
  78. package/dist/js/core/validation-focus.d.ts.map +1 -0
  79. package/dist/js/modules/address/api.d.ts +1 -0
  80. package/dist/js/modules/address/api.d.ts.map +1 -1
  81. package/dist/js/modules/address/constants.d.ts +22 -1
  82. package/dist/js/modules/address/constants.d.ts.map +1 -1
  83. package/dist/js/modules/address/host.d.ts +2 -2
  84. package/dist/js/modules/address/host.d.ts.map +1 -1
  85. package/dist/js/modules/address/loqate.d.ts.map +1 -1
  86. package/dist/js/modules/fields/address-country.d.ts +3 -0
  87. package/dist/js/modules/fields/address-country.d.ts.map +1 -0
  88. package/dist/js/modules/fields/address-state.d.ts +3 -0
  89. package/dist/js/modules/fields/address-state.d.ts.map +1 -0
  90. package/dist/js/modules/fields/calculations.d.ts.map +1 -1
  91. package/dist/js/modules/fields/combobox.d.ts +17 -0
  92. package/dist/js/modules/fields/combobox.d.ts.map +1 -0
  93. package/dist/js/modules/fields/custom-google-maps.d.ts +64 -0
  94. package/dist/js/modules/fields/custom-google-maps.d.ts.map +1 -0
  95. package/dist/js/modules/fields/custom-link.d.ts +3 -0
  96. package/dist/js/modules/fields/custom-link.d.ts.map +1 -0
  97. package/dist/js/modules/fields/custom-maps.d.ts +3 -0
  98. package/dist/js/modules/fields/custom-maps.d.ts.map +1 -0
  99. package/dist/js/modules/fields/date-picker.d.ts.map +1 -1
  100. package/dist/js/modules/fields/index.d.ts.map +1 -1
  101. package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
  102. package/dist/js/submit/pipeline.d.ts.map +1 -1
  103. package/dist/js/utils/country-from-ip.d.ts +7 -0
  104. package/dist/js/utils/country-from-ip.d.ts.map +1 -0
  105. package/dist/js/utils/field-references.d.ts +1 -0
  106. package/dist/js/utils/field-references.d.ts.map +1 -1
  107. package/dist/js/utils/field-references.row-scope.d.ts +11 -0
  108. package/dist/js/utils/field-references.row-scope.d.ts.map +1 -0
  109. package/package.json +6 -3
  110. package/src/css/formie-theme.css +2 -0
  111. package/src/css/theme/fields/_address-state.css +94 -0
  112. package/src/css/theme/fields/_combobox.css +246 -0
  113. package/src/css/theme/fields/_select.css +13 -0
  114. package/src/js/core/create-formie-client.ts +27 -0
  115. package/src/js/core/submit-result-ui.ts +4 -4
  116. package/src/js/core/validation-focus.ts +117 -0
  117. package/src/js/modules/address/api.ts +7 -0
  118. package/src/js/modules/address/constants.ts +82 -2
  119. package/src/js/modules/address/host.ts +16 -6
  120. package/src/js/modules/address/loqate.ts +3 -17
  121. package/src/js/modules/fields/address-country.ts +156 -0
  122. package/src/js/modules/fields/address-state.ts +946 -0
  123. package/src/js/modules/fields/calculations.ts +32 -0
  124. package/src/js/modules/fields/combobox.ts +188 -0
  125. package/src/js/modules/fields/custom-google-maps.ts +416 -0
  126. package/src/js/modules/fields/custom-link.ts +95 -0
  127. package/src/js/modules/fields/custom-maps.ts +339 -0
  128. package/src/js/modules/fields/date-picker.ts +96 -3
  129. package/src/js/modules/fields/index.ts +6 -0
  130. package/src/js/modules/fields/phone-country.ts +6 -0
  131. package/src/js/submit/pipeline.ts +11 -1
  132. package/src/js/utils/country-from-ip.ts +54 -0
  133. package/src/js/utils/field-references.row-scope.ts +303 -0
  134. package/src/js/utils/field-references.ts +1 -0
  135. package/dist/chunks/address-finder-BU4roxT6.js +0 -37
  136. package/dist/chunks/bpoint-uNg5j6rt.js +0 -26
  137. package/dist/chunks/calculations-C9t6Zop9.js +0 -147
  138. package/dist/chunks/captcha-eu-BAPzHNts.js +0 -34
  139. package/dist/chunks/checkbox-radio-CHeLNVIK.js +0 -141
  140. package/dist/chunks/conditions-DKxVCsjD.js +0 -459
  141. package/dist/chunks/date-picker-B3iQ797Z.js +0 -4994
  142. package/dist/chunks/eway-DCielIML.js +0 -74
  143. package/dist/chunks/file-upload-DP2TOUYV.js +0 -316
  144. package/dist/chunks/friendly-captcha-v1-DdlcGiFY.js +0 -33
  145. package/dist/chunks/friendly-captcha-v2-76w-YnFm.js +0 -38
  146. package/dist/chunks/go-cardless-rwCkzbNK.js +0 -24
  147. package/dist/chunks/google-address-BD8IZ86V.js +0 -181
  148. package/dist/chunks/hcaptcha-BNNTL6uf.js +0 -56
  149. package/dist/chunks/hidden-CW6Rcf1N.js +0 -31
  150. package/dist/chunks/index-BEsfee-D.js +0 -1839
  151. package/dist/chunks/index-BP7Unm5Y.js +0 -863
  152. package/dist/chunks/index-D6npYxvr.js +0 -4423
  153. package/dist/chunks/loqate-B-ZNS65B.js +0 -55
  154. package/dist/chunks/mollie-BDAZ-1X4.js +0 -24
  155. package/dist/chunks/moneris-B1b0uEtY.js +0 -101
  156. package/dist/chunks/opayo-CC5hraHo.js +0 -122
  157. package/dist/chunks/paddle-B4GmrDsq.js +0 -60
  158. package/dist/chunks/paypal-BFS6XsIS.js +0 -100
  159. package/dist/chunks/payway-DekoJead.js +0 -56
  160. package/dist/chunks/phone-country-Cm5YUeXI.js +0 -3765
  161. package/dist/chunks/place-kit-DizZWV_e.js +0 -42
  162. package/dist/chunks/placekit-autocomplete.esm-LAwfbPZl.js +0 -1299
  163. package/dist/chunks/recaptcha-enterprise-BbOvO7JZ.js +0 -71
  164. package/dist/chunks/recaptcha-shared-Dwg1eVFS.js +0 -15
  165. package/dist/chunks/recaptcha-v2-checkbox-SY3ti5iQ.js +0 -41
  166. package/dist/chunks/recaptcha-v2-invisible-DSmQnn6c.js +0 -59
  167. package/dist/chunks/recaptcha-v3-DNh7YCnR.js +0 -28
  168. package/dist/chunks/repeater-DM0vuFNQ.js +0 -124
  169. package/dist/chunks/rich-text-C8aGYp70.js +0 -304
  170. package/dist/chunks/scripts-CKTFtxPO.js +0 -36
  171. package/dist/chunks/sdk-3SicTWby.js +0 -1559
  172. package/dist/chunks/shared--BU5CFoL.js +0 -80
  173. package/dist/chunks/signature-pFhZfH7d.js +0 -550
  174. package/dist/chunks/snaptcha-VuOUOaLj.js +0 -8
  175. package/dist/chunks/square-BR9pvToy.js +0 -46
  176. package/dist/chunks/stripe-OHUwoV9g.js +0 -202
  177. package/dist/chunks/styles-C3aqgtek.js +0 -21
  178. package/dist/chunks/summary-Da7Ktg3N.js +0 -126
  179. package/dist/chunks/table-C0PmR9bI.js +0 -95
  180. package/dist/chunks/text-limit-mLEwZDIa.js +0 -144
  181. package/dist/chunks/turnstile-9tM7q_LK.js +0 -59
  182. package/dist/chunks/utils-BntI3W4S.js +0 -6149
@@ -0,0 +1,95 @@
1
+ import type { FormieModuleDefinition } from '#contracts/modules';
2
+ import { getModuleFieldTarget, observeMatchingElements } from '#modules/fields/shared';
3
+
4
+ const MODULE_ID = 'custom-link';
5
+ const LINK_SELECTOR = '[data-formie-custom-link]';
6
+
7
+ type LinkInputAttributes = {
8
+ type: string;
9
+ inputMode: string;
10
+ autocomplete: string;
11
+ };
12
+
13
+ function flag(element: HTMLElement, name: string): boolean {
14
+ return element.getAttribute(name) === '1';
15
+ }
16
+
17
+ function getValueInputAttributes(link: HTMLElement, type: string): LinkInputAttributes {
18
+ if (type === 'email') {
19
+ return {
20
+ type: 'email',
21
+ inputMode: 'email',
22
+ autocomplete: 'email',
23
+ };
24
+ }
25
+
26
+ if (type === 'tel' || type === 'sms') {
27
+ return {
28
+ type: 'tel',
29
+ inputMode: 'tel',
30
+ autocomplete: 'tel',
31
+ };
32
+ }
33
+
34
+ const strictUrl = !flag(link, 'data-formie-custom-link-allow-root-relative') &&
35
+ !flag(link, 'data-formie-custom-link-allow-anchors') &&
36
+ !flag(link, 'data-formie-custom-link-allow-custom-schemes');
37
+
38
+ return {
39
+ type: strictUrl ? 'url' : 'text',
40
+ inputMode: 'url',
41
+ autocomplete: 'url',
42
+ };
43
+ }
44
+
45
+ function isCustomLink(element: Element): element is HTMLElement {
46
+ return element instanceof HTMLElement && element.matches(LINK_SELECTOR);
47
+ }
48
+
49
+ function initCustomLink(link: HTMLElement): () => void {
50
+ const typeInput = link.querySelector('[data-formie-custom-link-type]');
51
+ const valueInput = link.querySelector('[data-formie-custom-link-value]');
52
+
53
+ if (!(typeInput instanceof HTMLSelectElement) || !(valueInput instanceof HTMLInputElement)) {
54
+ return () => {};
55
+ }
56
+
57
+ const syncValueInput = (): void => {
58
+ const attributes = getValueInputAttributes(link, typeInput.value);
59
+ valueInput.type = attributes.type;
60
+ valueInput.inputMode = attributes.inputMode;
61
+ valueInput.setAttribute('autocomplete', attributes.autocomplete);
62
+ };
63
+
64
+ syncValueInput();
65
+ typeInput.addEventListener('change', syncValueInput);
66
+
67
+ return () => {
68
+ typeInput.removeEventListener('change', syncValueInput);
69
+ };
70
+ }
71
+
72
+ export const customLinkModule: FormieModuleDefinition = {
73
+ id: MODULE_ID,
74
+ kind: 'field',
75
+ match: ({ target }) => {
76
+ return target instanceof Element && (
77
+ target.matches(LINK_SELECTOR) ||
78
+ !!target.querySelector(LINK_SELECTOR)
79
+ );
80
+ },
81
+ setup: async(ctx) => {
82
+ const field = getModuleFieldTarget(ctx);
83
+ const root = field || ctx.target;
84
+
85
+ if (!(root instanceof Element)) {
86
+ return;
87
+ }
88
+
89
+ const cleanup = observeMatchingElements(root, LINK_SELECTOR, isCustomLink, initCustomLink);
90
+
91
+ return {
92
+ destroy: cleanup,
93
+ };
94
+ },
95
+ };
@@ -0,0 +1,339 @@
1
+ import type { FormieModuleDefinition } from '#contracts/modules';
2
+ import type * as Leaflet from 'leaflet';
3
+ import leafletCss from 'leaflet/dist/leaflet.css?inline';
4
+ import { getModuleFieldTarget, observeMatchingElements } from '#modules/fields/shared';
5
+ import { ensureModuleStyles } from '#modules/styles';
6
+
7
+ const MODULE_ID = 'custom-maps';
8
+ const MAPS_SELECTOR = '[data-formie-custom-maps]';
9
+
10
+ ensureModuleStyles(MODULE_ID, [leafletCss]);
11
+
12
+ type LatLng = {
13
+ lat: number;
14
+ lng: number;
15
+ };
16
+
17
+ type MapsSettings = {
18
+ mapTiles?: string;
19
+ mapToken?: string | Record<string, string>;
20
+ geoService?: string;
21
+ geoToken?: string | Record<string, string>;
22
+ defaultLat?: number | null;
23
+ defaultLng?: number | null;
24
+ defaultZoom?: number | null;
25
+ minZoom?: number | null;
26
+ maxZoom?: number | null;
27
+ country?: string | null;
28
+ };
29
+
30
+ type MapsElements = {
31
+ root: HTMLElement;
32
+ canvas: HTMLElement | null;
33
+ searchInput: HTMLInputElement | null;
34
+ addressInput: HTMLInputElement | null;
35
+ latInput: HTMLInputElement | null;
36
+ lngInput: HTMLInputElement | null;
37
+ zoomInput: HTMLInputElement | null;
38
+ partsInput: HTMLInputElement | null;
39
+ currentLocationButton: HTMLButtonElement | null;
40
+ };
41
+
42
+ type NominatimResult = {
43
+ display_name?: string;
44
+ lat?: string;
45
+ lon?: string;
46
+ address?: Record<string, string | undefined>;
47
+ };
48
+
49
+ let leafletPromise: Promise<typeof Leaflet> | null = null;
50
+
51
+ function isCustomMaps(element: Element): element is HTMLElement {
52
+ return element instanceof HTMLElement && element.matches(MAPS_SELECTOR);
53
+ }
54
+
55
+ function parseSettings(root: HTMLElement): MapsSettings {
56
+ const raw = root.getAttribute('data-formie-custom-maps-settings');
57
+
58
+ if (!raw) {
59
+ return {};
60
+ }
61
+
62
+ try {
63
+ return JSON.parse(raw) as MapsSettings;
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+
69
+ function findElements(root: HTMLElement): MapsElements {
70
+ return {
71
+ root,
72
+ canvas: root.querySelector<HTMLElement>('[data-formie-custom-maps-canvas]'),
73
+ searchInput: root.querySelector<HTMLInputElement>('[data-formie-custom-maps-search]'),
74
+ addressInput: root.querySelector<HTMLInputElement>('[data-formie-custom-map-address]'),
75
+ latInput: root.querySelector<HTMLInputElement>('[data-formie-custom-map-lat]'),
76
+ lngInput: root.querySelector<HTMLInputElement>('[data-formie-custom-map-lng]'),
77
+ zoomInput: root.querySelector<HTMLInputElement>('[data-formie-custom-map-zoom]'),
78
+ partsInput: root.querySelector<HTMLInputElement>('[data-formie-custom-maps-parts]'),
79
+ currentLocationButton: root.querySelector<HTMLButtonElement>('[data-formie-custom-maps-current-location]'),
80
+ };
81
+ }
82
+
83
+ function toNumber(value: string | number | null | undefined): number | null {
84
+ if (value === null || value === undefined || value === '') {
85
+ return null;
86
+ }
87
+
88
+ const number = Number(value);
89
+
90
+ return Number.isFinite(number) ? number : null;
91
+ }
92
+
93
+ function getCurrentLatLng(elements: MapsElements, settings: MapsSettings): LatLng {
94
+ return {
95
+ lat: toNumber(elements.latInput?.value) ?? settings.defaultLat ?? 51.272154,
96
+ lng: toNumber(elements.lngInput?.value) ?? settings.defaultLng ?? 0.514951,
97
+ };
98
+ }
99
+
100
+ function getCurrentZoom(elements: MapsElements, settings: MapsSettings): number {
101
+ return toNumber(elements.zoomInput?.value) ?? settings.defaultZoom ?? 15;
102
+ }
103
+
104
+ function setValue(input: HTMLInputElement | null, value: string | number | null | undefined): void {
105
+ if (!input) {
106
+ return;
107
+ }
108
+
109
+ input.value = value === null || value === undefined ? '' : String(value);
110
+ input.dispatchEvent(new Event('input', { bubbles: true }));
111
+ input.dispatchEvent(new Event('change', { bubbles: true }));
112
+ }
113
+
114
+ function syncCoordinates(elements: MapsElements, latLng: LatLng, zoom?: number): void {
115
+ setValue(elements.latInput, latLng.lat);
116
+ setValue(elements.lngInput, latLng.lng);
117
+
118
+ if (zoom !== undefined) {
119
+ setValue(elements.zoomInput, zoom);
120
+ }
121
+ }
122
+
123
+ function loadLeaflet(): Promise<typeof Leaflet> {
124
+ if (!leafletPromise) {
125
+ leafletPromise = import('leaflet');
126
+ }
127
+
128
+ return leafletPromise;
129
+ }
130
+
131
+ function getRasterTileUrl(settings: MapsSettings): string {
132
+ switch (settings.mapTiles) {
133
+ case 'openstreetmap':
134
+ case 'wikimedia':
135
+ return 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png';
136
+ case 'carto.light_all':
137
+ return 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
138
+ case 'carto.dark_all':
139
+ return 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
140
+ case 'carto.rastertiles/voyager':
141
+ default:
142
+ return 'https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png';
143
+ }
144
+ }
145
+
146
+ function mapNominatimParts(address: Record<string, string | undefined> | undefined): Record<string, string> {
147
+ if (!address) {
148
+ return {};
149
+ }
150
+
151
+ return {
152
+ number: address.house_number || '',
153
+ address: address.road || address.pedestrian || address.footway || address.path || address.neighbourhood || '',
154
+ city: address.city || address.town || address.village || address.suburb || '',
155
+ postcode: address.postcode || '',
156
+ county: address.county || '',
157
+ state: address.state || address.state_district || '',
158
+ country: address.country || '',
159
+ };
160
+ }
161
+
162
+ async function searchNominatim(query: string, settings: MapsSettings): Promise<{ latLng: LatLng; address: string; parts: Record<string, string> } | null> {
163
+ const params = new URLSearchParams({
164
+ format: 'jsonv2',
165
+ limit: '1',
166
+ addressdetails: '1',
167
+ q: query,
168
+ });
169
+
170
+ if (settings.country) {
171
+ params.set('countrycodes', settings.country.toLowerCase());
172
+ }
173
+
174
+ const response = await fetch(`https://nominatim.openstreetmap.org/search?${params.toString()}`);
175
+
176
+ if (!response.ok) {
177
+ return null;
178
+ }
179
+
180
+ const results = await response.json() as NominatimResult[];
181
+ const result = results[0];
182
+ const lat = toNumber(result?.lat);
183
+ const lng = toNumber(result?.lon);
184
+
185
+ if (!result || lat === null || lng === null) {
186
+ return null;
187
+ }
188
+
189
+ return {
190
+ latLng: { lat, lng },
191
+ address: result.display_name || query,
192
+ parts: mapNominatimParts(result.address),
193
+ };
194
+ }
195
+
196
+ function applySearchResult(elements: MapsElements, result: { latLng: LatLng; address: string; parts: Record<string, string> }, zoom: number): void {
197
+ setValue(elements.addressInput, result.address);
198
+ setValue(elements.searchInput, result.address);
199
+ setValue(elements.partsInput, JSON.stringify(result.parts));
200
+ syncCoordinates(elements, result.latLng, zoom);
201
+ }
202
+
203
+ async function initLeafletMap(elements: MapsElements, settings: MapsSettings): Promise<() => void> {
204
+ if (!elements.canvas) {
205
+ return () => {};
206
+ }
207
+
208
+ const L = await loadLeaflet();
209
+ const initialLatLng = getCurrentLatLng(elements, settings);
210
+ const initialZoom = getCurrentZoom(elements, settings);
211
+ const map = L.map(elements.canvas, {
212
+ minZoom: settings.minZoom ?? 3,
213
+ maxZoom: settings.maxZoom ?? 18,
214
+ }).setView([initialLatLng.lat, initialLatLng.lng], initialZoom);
215
+ const marker = L.marker([initialLatLng.lat, initialLatLng.lng], {
216
+ draggable: true,
217
+ }).addTo(map);
218
+
219
+ L.tileLayer(getRasterTileUrl(settings), {
220
+ maxZoom: settings.maxZoom ?? 18,
221
+ attribution: '&copy; OpenStreetMap contributors, &copy; CARTO',
222
+ }).addTo(map);
223
+
224
+ map.on('click', (event) => {
225
+ marker.setLatLng([event.latlng.lat, event.latlng.lng]);
226
+ syncCoordinates(elements, event.latlng, map.getZoom());
227
+ });
228
+
229
+ marker.on('dragend', (event) => {
230
+ const latLng = event.target.getLatLng();
231
+ syncCoordinates(elements, latLng, map.getZoom());
232
+ });
233
+
234
+ const submitSearch = async(): Promise<void> => {
235
+ const query = elements.searchInput?.value.trim();
236
+
237
+ if (!query) {
238
+ return;
239
+ }
240
+
241
+ const result = await searchNominatim(query, settings);
242
+
243
+ if (!result) {
244
+ return;
245
+ }
246
+
247
+ const zoom = getCurrentZoom(elements, settings);
248
+ applySearchResult(elements, result, zoom);
249
+ map.setView([result.latLng.lat, result.latLng.lng], zoom);
250
+ marker.setLatLng([result.latLng.lat, result.latLng.lng]);
251
+ };
252
+
253
+ const onSearchKeydown = (event: KeyboardEvent): void => {
254
+ if (event.key !== 'Enter') {
255
+ return;
256
+ }
257
+
258
+ event.preventDefault();
259
+ void submitSearch();
260
+ };
261
+
262
+ const onSearchBlur = (): void => {
263
+ void submitSearch();
264
+ };
265
+
266
+ elements.searchInput?.addEventListener('keydown', onSearchKeydown);
267
+ elements.searchInput?.addEventListener('blur', onSearchBlur);
268
+
269
+ const onCurrentLocationClick = (): void => {
270
+ navigator.geolocation?.getCurrentPosition((position) => {
271
+ const latLng = {
272
+ lat: position.coords.latitude,
273
+ lng: position.coords.longitude,
274
+ };
275
+ const zoom = getCurrentZoom(elements, settings);
276
+
277
+ syncCoordinates(elements, latLng, zoom);
278
+ map.setView([latLng.lat, latLng.lng], zoom);
279
+ marker.setLatLng([latLng.lat, latLng.lng]);
280
+ });
281
+ };
282
+
283
+ elements.currentLocationButton?.addEventListener('click', onCurrentLocationClick);
284
+
285
+ return () => {
286
+ elements.searchInput?.removeEventListener('keydown', onSearchKeydown);
287
+ elements.searchInput?.removeEventListener('blur', onSearchBlur);
288
+ elements.currentLocationButton?.removeEventListener('click', onCurrentLocationClick);
289
+ map.remove();
290
+ };
291
+ }
292
+
293
+ function initCustomMaps(root: HTMLElement): () => void {
294
+ const elements = findElements(root);
295
+ const settings = parseSettings(root);
296
+ let cleanup: (() => void) | null = null;
297
+ let destroyed = false;
298
+
299
+ if (root.getAttribute('data-formie-custom-map-hide-map') !== '1') {
300
+ void initLeafletMap(elements, settings).then((destroy) => {
301
+ if (destroyed) {
302
+ destroy();
303
+ return;
304
+ }
305
+
306
+ cleanup = destroy;
307
+ });
308
+ }
309
+
310
+ return () => {
311
+ destroyed = true;
312
+ cleanup?.();
313
+ };
314
+ }
315
+
316
+ export const customMapsModule: FormieModuleDefinition = {
317
+ id: MODULE_ID,
318
+ kind: 'field',
319
+ match: ({ target }) => {
320
+ return target instanceof Element && (
321
+ target.matches(MAPS_SELECTOR) ||
322
+ !!target.querySelector(MAPS_SELECTOR)
323
+ );
324
+ },
325
+ setup: async(ctx) => {
326
+ const field = getModuleFieldTarget(ctx);
327
+ const root = field || ctx.target;
328
+
329
+ if (!(root instanceof Element)) {
330
+ return;
331
+ }
332
+
333
+ const cleanup = observeMatchingElements(root, MAPS_SELECTOR, isCustomMaps, initCustomMaps);
334
+
335
+ return {
336
+ destroy: cleanup,
337
+ };
338
+ },
339
+ };
@@ -8,18 +8,20 @@ import { ensureModuleStyles } from '#modules/styles';
8
8
  import { createDebug } from '#utils/debug';
9
9
 
10
10
  const INPUT_SELECTOR = 'input[data-formie-date-datepicker-input]';
11
+ const RANGE_START_SELECTOR = 'input[data-formie-date-range-start-input]';
12
+ const RANGE_END_SELECTOR = 'input[data-formie-date-range-end-input]';
11
13
  const MODULE_ID = 'date-picker';
12
14
  const debug = createDebug('fields', 'date-picker');
13
15
 
14
- ensureModuleStyles(MODULE_ID, [flatpickrCss]);
15
-
16
16
  type DatePickerOptions = {
17
+ includeFlatpickrCss?: boolean;
17
18
  datePickerOptions?: Array<{ label?: string; value?: unknown }>;
18
19
  dateFormat?: string;
19
20
  timeFormat?: string;
20
21
  getIsDate?: boolean;
21
22
  getIsTime?: boolean;
22
23
  getIsDateTime?: boolean;
24
+ collectMode?: string;
23
25
  locale?: string;
24
26
  minDate?: string | null;
25
27
  maxDate?: string | null;
@@ -34,6 +36,11 @@ type FlatpickrInput = HTMLInputElement & {
34
36
  _formieFlatpickr?: FlatpickrInstanceLike;
35
37
  };
36
38
 
39
+ type FlatpickrChangeInstance = {
40
+ input: HTMLInputElement;
41
+ altInput?: HTMLInputElement;
42
+ };
43
+
37
44
  function attributesPlugin() {
38
45
  return (instance: { input: HTMLInputElement; altInput?: HTMLInputElement | undefined; loadedPlugins: string[] }) => {
39
46
  return {
@@ -168,9 +175,61 @@ function getCustomOptions(options: DatePickerOptions): Record<string, unknown> {
168
175
  return result;
169
176
  }
170
177
 
178
+ function pad(value: number): string {
179
+ return String(value).padStart(2, '0');
180
+ }
181
+
182
+ function formatSubmissionDate(date: Date, options: DatePickerOptions): string {
183
+ const year = date.getFullYear();
184
+ const month = pad(date.getMonth() + 1);
185
+ const day = pad(date.getDate());
186
+
187
+ if (!options.getIsTime && !options.getIsDateTime) {
188
+ return `${year}-${month}-${day}`;
189
+ }
190
+
191
+ const hours = pad(date.getHours());
192
+ const minutes = pad(date.getMinutes());
193
+ const seconds = pad(date.getSeconds());
194
+
195
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
196
+ }
197
+
198
+ function parseSubmissionDate(value: string): Date | null {
199
+ const parsed = new Date(value);
200
+
201
+ if (Number.isNaN(parsed.getTime())) {
202
+ return null;
203
+ }
204
+
205
+ return parsed;
206
+ }
207
+
208
+ function syncRangeHiddenInputs(
209
+ selectedDates: Date[],
210
+ startInput: HTMLInputElement | null,
211
+ endInput: HTMLInputElement | null,
212
+ options: DatePickerOptions,
213
+ ): void {
214
+ if (startInput) {
215
+ startInput.value = selectedDates[0] ? formatSubmissionDate(selectedDates[0], options) : '';
216
+ startInput.dispatchEvent(new Event('input', { bubbles: true }));
217
+ }
218
+
219
+ if (endInput) {
220
+ endInput.value = selectedDates[1] ? formatSubmissionDate(selectedDates[1], options) : '';
221
+ endInput.dispatchEvent(new Event('input', { bubbles: true }));
222
+ }
223
+ }
224
+
171
225
  function initDatePicker(input: FlatpickrInput, options: DatePickerOptions): () => void {
172
226
  input._formieFlatpickr?.destroy();
173
227
 
228
+ const fieldContainer = input.closest('[data-formie-field-handle]');
229
+ const isRange = options.collectMode === 'range' || input.hasAttribute('data-formie-date-range-input');
230
+ const startInput = fieldContainer?.querySelector(RANGE_START_SELECTOR);
231
+ const endInput = fieldContainer?.querySelector(RANGE_END_SELECTOR);
232
+
174
233
  const defaultOptions: Record<string, unknown> = {
175
234
  disableMobile: true,
176
235
  allowInput: true,
@@ -183,7 +242,16 @@ function initDatePicker(input: FlatpickrInput, options: DatePickerOptions): () =
183
242
  maxDate: normalizeOffsetDate(options.maxDate, 'max'),
184
243
  plugins: [attributesPlugin()],
185
244
  locale: getLocale(options.locale),
186
- onChange: (_selectedDates: unknown, _dateStr: string, instance: { input: HTMLInputElement; altInput?: HTMLInputElement }) => {
245
+ onChange: (selectedDates: Date[], _dateStr: string, instance: FlatpickrChangeInstance) => {
246
+ if (isRange) {
247
+ syncRangeHiddenInputs(
248
+ selectedDates,
249
+ startInput instanceof HTMLInputElement ? startInput : null,
250
+ endInput instanceof HTMLInputElement ? endInput : null,
251
+ options,
252
+ );
253
+ }
254
+
187
255
  // Mirror changes back through bubbling input events so validation,
188
256
  // conditions, and calculations respond to picker-driven updates.
189
257
  instance.input.dispatchEvent(new Event('input', { bubbles: true }));
@@ -204,11 +272,30 @@ function initDatePicker(input: FlatpickrInput, options: DatePickerOptions): () =
204
272
  defaultOptions.noCalendar = true;
205
273
  }
206
274
 
275
+ if (isRange) {
276
+ defaultOptions.mode = 'range';
277
+
278
+ const startDate = startInput instanceof HTMLInputElement && startInput.value
279
+ ? parseSubmissionDate(startInput.value)
280
+ : null;
281
+ const endDate = endInput instanceof HTMLInputElement && endInput.value
282
+ ? parseSubmissionDate(endInput.value)
283
+ : null;
284
+
285
+ if (startDate) {
286
+ defaultOptions.defaultDate = endDate ? [startDate, endDate] : [startDate];
287
+ }
288
+ }
289
+
207
290
  const mergedOptions = {
208
291
  ...defaultOptions,
209
292
  ...getCustomOptions(options),
210
293
  };
211
294
 
295
+ if (isRange) {
296
+ mergedOptions.mode = 'range';
297
+ }
298
+
212
299
  dispatchFieldEvent(input, MODULE_ID, 'before-init', {
213
300
  datepicker: input,
214
301
  options: mergedOptions,
@@ -218,6 +305,7 @@ function initDatePicker(input: FlatpickrInput, options: DatePickerOptions): () =
218
305
  input._formieFlatpickr = instance;
219
306
  debug.log('Initialized.', {
220
307
  inputName: input.name,
308
+ isRange,
221
309
  });
222
310
 
223
311
  dispatchFieldEvent(input, MODULE_ID, 'after-init', {
@@ -242,6 +330,11 @@ export const datePickerModule: FormieModuleDefinition = {
242
330
  },
243
331
  setup: async (ctx) => {
244
332
  const options = (ctx.options || {}) as DatePickerOptions;
333
+
334
+ if (options.includeFlatpickrCss !== false) {
335
+ ensureModuleStyles(MODULE_ID, [flatpickrCss]);
336
+ }
337
+
245
338
  const fields = getModuleFieldContainers(ctx);
246
339
  const cleanups = fields.map((field) => {
247
340
  const input = field.querySelector(INPUT_SELECTOR);
@@ -5,11 +5,17 @@ export const builtinFieldModuleLoaders: Record<string, () => Promise<FormieModul
5
5
  // truth for lazy-loading first-party field enhancements.
6
6
  'calculations': () => import('#modules/fields/calculations').then((module) => module.calculationsModule),
7
7
  'checkbox-radio': () => import('#modules/fields/checkbox-radio').then((module) => module.checkboxRadioModule),
8
+ 'combobox': () => import('#modules/fields/combobox').then((module) => module.comboboxModule),
8
9
  'conditions': () => import('#modules/fields/conditions').then((module) => module.conditionsModule),
10
+ 'custom-google-maps': () => import('#modules/fields/custom-google-maps').then((module) => module.customGoogleMapsModule),
11
+ 'custom-link': () => import('#modules/fields/custom-link').then((module) => module.customLinkModule),
12
+ 'custom-maps': () => import('#modules/fields/custom-maps').then((module) => module.customMapsModule),
9
13
  'date-picker': () => import('#modules/fields/date-picker').then((module) => module.datePickerModule),
10
14
  'file-upload': () => import('#modules/fields/file-upload').then((module) => module.fileUploadModule),
11
15
  'hidden': () => import('#modules/fields/hidden').then((module) => module.hiddenModule),
12
16
  'phone-country': () => import('#modules/fields/phone-country').then((module) => module.phoneCountryModule),
17
+ 'address-country': () => import('#modules/fields/address-country').then((module) => module.addressCountryModule),
18
+ 'address-state': () => import('#modules/fields/address-state').then((module) => module.addressStateModule),
13
19
  'repeater': () => import('#modules/fields/repeater').then((module) => module.repeaterModule),
14
20
  'rich-text': () => import('#modules/fields/rich-text').then((module) => module.richTextModule),
15
21
  'signature': () => import('#modules/fields/signature').then((module) => module.signatureModule),
@@ -4,6 +4,7 @@ import intlTelInputCss from 'intl-tel-input/styles?inline';
4
4
  import type { FormieModuleDefinition } from '#contracts/modules';
5
5
  import { dispatchFieldEvent, getModuleFieldContainers, releaseFormValidators, retainFormValidators } from '#modules/fields/shared';
6
6
  import { ensureModuleStyles } from '#modules/styles';
7
+ import { createGeoIpLookup } from '#utils/country-from-ip';
7
8
  import { createDebug } from '#utils/debug';
8
9
 
9
10
  const PHONE_SELECTOR = 'input[type="tel"][data-formie-phone-input]';
@@ -18,6 +19,8 @@ ensureModuleStyles(MODULE_ID, [intlTelInputCss]);
18
19
  type PhoneCountryOptions = {
19
20
  countryDefaultValue?: string;
20
21
  countryAllowed?: string[];
22
+ countryPreselectFromIp?: boolean;
23
+ countryFromIpAction?: string;
21
24
  language?: string;
22
25
  };
23
26
 
@@ -111,6 +114,9 @@ function buildOptions(options: PhoneCountryOptions): Record<string, unknown> {
111
114
 
112
115
  if (options.countryDefaultValue) {
113
116
  intlOptions.initialCountry = options.countryDefaultValue.toLowerCase();
117
+ } else if (options.countryPreselectFromIp) {
118
+ intlOptions.initialCountry = 'auto';
119
+ intlOptions.geoIpLookup = createGeoIpLookup(options.countryFromIpAction);
114
120
  }
115
121
 
116
122
  if (Array.isArray(intlOptions.onlyCountries) && typeof intlOptions.initialCountry === 'string') {
@@ -250,7 +250,17 @@ export async function runSubmitPipeline(
250
250
  const errors = options.validator.submit(final ? ctx.form : scope, { final });
251
251
 
252
252
  if (errors.length > 0) {
253
- errors[0]?.input.focus();
253
+ const firstErrorInput = errors[0]?.input;
254
+
255
+ if (firstErrorInput) {
256
+ firstErrorInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
257
+
258
+ try {
259
+ firstErrorInput.focus({ preventScroll: true });
260
+ } catch {
261
+ firstErrorInput.focus();
262
+ }
263
+ }
254
264
 
255
265
  return {
256
266
  ok: false,