@verbb/formie-browser 1.0.3 → 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 (206) hide show
  1. package/CHANGELOG.md +65 -17
  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/field-error-container.d.ts +4 -0
  77. package/dist/js/core/field-error-container.d.ts.map +1 -0
  78. package/dist/js/core/submit-result-state.d.ts.map +1 -1
  79. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  80. package/dist/js/core/validation-focus.d.ts +4 -0
  81. package/dist/js/core/validation-focus.d.ts.map +1 -0
  82. package/dist/js/modules/address/api.d.ts +1 -0
  83. package/dist/js/modules/address/api.d.ts.map +1 -1
  84. package/dist/js/modules/address/constants.d.ts +22 -1
  85. package/dist/js/modules/address/constants.d.ts.map +1 -1
  86. package/dist/js/modules/address/host.d.ts +2 -2
  87. package/dist/js/modules/address/host.d.ts.map +1 -1
  88. package/dist/js/modules/address/loqate.d.ts.map +1 -1
  89. package/dist/js/modules/fields/address-country.d.ts +3 -0
  90. package/dist/js/modules/fields/address-country.d.ts.map +1 -0
  91. package/dist/js/modules/fields/address-state.d.ts +3 -0
  92. package/dist/js/modules/fields/address-state.d.ts.map +1 -0
  93. package/dist/js/modules/fields/calculations.d.ts.map +1 -1
  94. package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
  95. package/dist/js/modules/fields/combobox.d.ts +17 -0
  96. package/dist/js/modules/fields/combobox.d.ts.map +1 -0
  97. package/dist/js/modules/fields/custom-google-maps.d.ts +64 -0
  98. package/dist/js/modules/fields/custom-google-maps.d.ts.map +1 -0
  99. package/dist/js/modules/fields/custom-link.d.ts +3 -0
  100. package/dist/js/modules/fields/custom-link.d.ts.map +1 -0
  101. package/dist/js/modules/fields/custom-maps.d.ts +3 -0
  102. package/dist/js/modules/fields/custom-maps.d.ts.map +1 -0
  103. package/dist/js/modules/fields/date-picker.d.ts.map +1 -1
  104. package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
  105. package/dist/js/modules/fields/index.d.ts.map +1 -1
  106. package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
  107. package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
  108. package/dist/js/submit/pipeline.d.ts.map +1 -1
  109. package/dist/js/utils/country-from-ip.d.ts +7 -0
  110. package/dist/js/utils/country-from-ip.d.ts.map +1 -0
  111. package/dist/js/utils/field-references.d.ts +1 -0
  112. package/dist/js/utils/field-references.d.ts.map +1 -1
  113. package/dist/js/utils/field-references.row-scope.d.ts +11 -0
  114. package/dist/js/utils/field-references.row-scope.d.ts.map +1 -0
  115. package/dist/js/utils/i18n.d.ts.map +1 -1
  116. package/dist/js/validation/rules/email.d.ts.map +1 -1
  117. package/dist/js/validation/rules/number.d.ts.map +1 -1
  118. package/dist/js/validation/validator.d.ts.map +1 -1
  119. package/package.json +6 -3
  120. package/src/css/formie-theme.css +2 -0
  121. package/src/css/theme/fields/_address-state.css +94 -0
  122. package/src/css/theme/fields/_combobox.css +246 -0
  123. package/src/css/theme/fields/_select.css +13 -0
  124. package/src/js/core/create-formie-client.ts +27 -0
  125. package/src/js/core/field-error-container.ts +39 -0
  126. package/src/js/core/submit-result-state.ts +12 -2
  127. package/src/js/core/submit-result-ui.ts +7 -13
  128. package/src/js/core/validation-focus.ts +117 -0
  129. package/src/js/modules/address/api.ts +7 -0
  130. package/src/js/modules/address/constants.ts +82 -2
  131. package/src/js/modules/address/host.ts +16 -6
  132. package/src/js/modules/address/loqate.ts +3 -17
  133. package/src/js/modules/fields/address-country.ts +156 -0
  134. package/src/js/modules/fields/address-state.ts +946 -0
  135. package/src/js/modules/fields/calculations.ts +32 -0
  136. package/src/js/modules/fields/checkbox-radio.ts +21 -15
  137. package/src/js/modules/fields/combobox.ts +188 -0
  138. package/src/js/modules/fields/custom-google-maps.ts +416 -0
  139. package/src/js/modules/fields/custom-link.ts +95 -0
  140. package/src/js/modules/fields/custom-maps.ts +339 -0
  141. package/src/js/modules/fields/date-picker.ts +96 -3
  142. package/src/js/modules/fields/file-upload.ts +12 -9
  143. package/src/js/modules/fields/index.ts +6 -0
  144. package/src/js/modules/fields/phone-country.ts +6 -0
  145. package/src/js/modules/fields/text-limit.ts +87 -22
  146. package/src/js/submit/pipeline.ts +11 -1
  147. package/src/js/utils/country-from-ip.ts +54 -0
  148. package/src/js/utils/field-references.row-scope.ts +303 -0
  149. package/src/js/utils/field-references.ts +1 -0
  150. package/src/js/utils/i18n.ts +184 -5
  151. package/src/js/validation/rules/email.ts +3 -2
  152. package/src/js/validation/rules/match.ts +2 -2
  153. package/src/js/validation/rules/minmax.ts +4 -4
  154. package/src/js/validation/rules/number.ts +9 -5
  155. package/src/js/validation/rules/pattern.ts +4 -4
  156. package/src/js/validation/rules/required.ts +1 -1
  157. package/src/js/validation/rules/url.ts +1 -1
  158. package/src/js/validation/validator.ts +7 -7
  159. package/dist/chunks/address-finder-BjMuEiwF.js +0 -37
  160. package/dist/chunks/bpoint-KudsBCo2.js +0 -26
  161. package/dist/chunks/calculations-DlGSkQ3W.js +0 -147
  162. package/dist/chunks/captcha-eu-CterYFok.js +0 -34
  163. package/dist/chunks/checkbox-radio-CBvgk3hR.js +0 -136
  164. package/dist/chunks/conditions-DYy1-Hwt.js +0 -459
  165. package/dist/chunks/date-picker-BlD--zfe.js +0 -4994
  166. package/dist/chunks/eway-qwc-B5Us.js +0 -74
  167. package/dist/chunks/file-upload-CIgHrXfp.js +0 -316
  168. package/dist/chunks/friendly-captcha-v1-EDFac33c.js +0 -33
  169. package/dist/chunks/friendly-captcha-v2-BDpSvFGR.js +0 -38
  170. package/dist/chunks/go-cardless-DuDMVsnz.js +0 -24
  171. package/dist/chunks/google-address-Bmjp01pH.js +0 -181
  172. package/dist/chunks/hcaptcha-BTQ6An6g.js +0 -56
  173. package/dist/chunks/hidden-DbPdOxKb.js +0 -31
  174. package/dist/chunks/index-BEsfee-D.js +0 -1839
  175. package/dist/chunks/index-BP7Unm5Y.js +0 -863
  176. package/dist/chunks/index-BcqwpPNE.js +0 -4314
  177. package/dist/chunks/loqate-BdNGv93o.js +0 -55
  178. package/dist/chunks/mollie-DFOjTuGX.js +0 -24
  179. package/dist/chunks/moneris-BbFfl-EL.js +0 -101
  180. package/dist/chunks/opayo-CmgYa9ya.js +0 -122
  181. package/dist/chunks/paddle-BUi7w1_o.js +0 -60
  182. package/dist/chunks/paypal-A-ePfh6k.js +0 -100
  183. package/dist/chunks/payway-BqFCbhyo.js +0 -56
  184. package/dist/chunks/phone-country-DPMiqk4L.js +0 -3765
  185. package/dist/chunks/place-kit-CX7tamAg.js +0 -42
  186. package/dist/chunks/placekit-autocomplete.esm-LAwfbPZl.js +0 -1299
  187. package/dist/chunks/recaptcha-enterprise-DxF6cW95.js +0 -71
  188. package/dist/chunks/recaptcha-shared-DhGcbiaq.js +0 -15
  189. package/dist/chunks/recaptcha-v2-checkbox-R9EfbZ4k.js +0 -41
  190. package/dist/chunks/recaptcha-v2-invisible-DV9hswAp.js +0 -59
  191. package/dist/chunks/recaptcha-v3-V0xc-Edh.js +0 -28
  192. package/dist/chunks/repeater-DKUT6TGI.js +0 -124
  193. package/dist/chunks/rich-text-SH7KiaKH.js +0 -304
  194. package/dist/chunks/scripts-D9N2d5rN.js +0 -36
  195. package/dist/chunks/sdk-3SicTWby.js +0 -1559
  196. package/dist/chunks/shared-BmntPmnW.js +0 -80
  197. package/dist/chunks/signature-BA3tQsGT.js +0 -550
  198. package/dist/chunks/snaptcha-DV7p6J9k.js +0 -8
  199. package/dist/chunks/square-Bs-ndqYK.js +0 -46
  200. package/dist/chunks/stripe-DkZAHxtn.js +0 -202
  201. package/dist/chunks/styles-C3aqgtek.js +0 -21
  202. package/dist/chunks/summary-Dq2i9TQk.js +0 -126
  203. package/dist/chunks/table-Cu4WfVcw.js +0 -95
  204. package/dist/chunks/text-limit-2ycmB-Ae.js +0 -135
  205. package/dist/chunks/turnstile-CId90tRr.js +0 -59
  206. package/dist/chunks/utils-BntI3W4S.js +0 -6149
@@ -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);
@@ -239,9 +239,10 @@ function registerValidators(form: HTMLFormElement | null): void {
239
239
 
240
240
  return input.files.length <= limit;
241
241
  }, ({ input, t }) => {
242
- return t('Choose up to {files} files.', {
243
- files: input.getAttribute('data-formie-file-limit') || '',
244
- });
242
+ return input.getAttribute('data-formie-validation-max-files-message')
243
+ ?? t('Choose up to {files} files.', {
244
+ files: input.getAttribute('data-formie-file-limit') || '',
245
+ });
245
246
  });
246
247
 
247
248
  validator.addValidator('fileSizeMinLimit', ({ input }) => {
@@ -256,9 +257,10 @@ function registerValidators(form: HTMLFormElement | null): void {
256
257
  return file.size >= sizeBytes;
257
258
  });
258
259
  }, ({ input, t }) => {
259
- return t('File must be larger than {filesize} MB.', {
260
- filesize: input.getAttribute('data-formie-size-min-limit') || '',
261
- });
260
+ return input.getAttribute('data-formie-validation-min-file-size-message')
261
+ ?? t('File must be larger than {filesize} MB.', {
262
+ filesize: input.getAttribute('data-formie-size-min-limit') || '',
263
+ });
262
264
  });
263
265
 
264
266
  validator.addValidator('fileSizeMaxLimit', ({ input }) => {
@@ -273,9 +275,10 @@ function registerValidators(form: HTMLFormElement | null): void {
273
275
  return file.size <= sizeBytes;
274
276
  });
275
277
  }, ({ input, t }) => {
276
- return t('File must be smaller than {filesize} MB.', {
277
- filesize: input.getAttribute('data-formie-size-max-limit') || '',
278
- });
278
+ return input.getAttribute('data-formie-validation-max-file-size-message')
279
+ ?? t('File must be smaller than {filesize} MB.', {
280
+ filesize: input.getAttribute('data-formie-size-max-limit') || '',
281
+ });
279
282
  });
280
283
  });
281
284
  }
@@ -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') {