@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,416 @@
1
+ import type { FormieModuleDefinition } from '#contracts/modules';
2
+ import { getModuleFieldTarget, observeMatchingElements } from '#modules/fields/shared';
3
+
4
+ const MODULE_ID = 'custom-google-maps';
5
+ const GOOGLE_MAPS_SELECTOR = '[data-formie-custom-google-maps]';
6
+
7
+ type LatLng = {
8
+ lat: number;
9
+ lng: number;
10
+ };
11
+
12
+ type GoogleMapsSettings = {
13
+ apiUrl?: string;
14
+ defaultLat?: number | null;
15
+ defaultLng?: number | null;
16
+ defaultZoom?: number | null;
17
+ minZoom?: number | null;
18
+ maxZoom?: number | null;
19
+ country?: string | null;
20
+ };
21
+
22
+ type GoogleMapsElements = {
23
+ root: HTMLElement;
24
+ canvas: HTMLElement | null;
25
+ searchInput: HTMLInputElement | null;
26
+ currentLocationButton: HTMLButtonElement | null;
27
+ inputs: Record<string, HTMLInputElement>;
28
+ };
29
+
30
+ type GoogleAddressComponent = {
31
+ long_name: string;
32
+ short_name: string;
33
+ types: string[];
34
+ };
35
+
36
+ type GooglePlace = {
37
+ formatted_address?: string;
38
+ name?: string;
39
+ place_id?: string;
40
+ address_components?: GoogleAddressComponent[];
41
+ geometry?: {
42
+ location?: {
43
+ lat: () => number;
44
+ lng: () => number;
45
+ };
46
+ };
47
+ };
48
+
49
+ type GoogleMapsNamespace = {
50
+ maps: {
51
+ Map: new(element: HTMLElement, options: Record<string, unknown>) => GoogleMap;
52
+ Marker: new(options: Record<string, unknown>) => GoogleMarker;
53
+ LatLng: new(lat: number, lng: number) => unknown;
54
+ importLibrary?: (name: string) => Promise<unknown>;
55
+ places: {
56
+ Autocomplete: new(input: HTMLInputElement, options?: Record<string, unknown>) => GoogleAutocomplete;
57
+ };
58
+ };
59
+ };
60
+
61
+ type GoogleMap = {
62
+ setCenter: (latLng: LatLng) => void;
63
+ setZoom: (zoom: number) => void;
64
+ getZoom: () => number | undefined;
65
+ addListener: (event: string, callback: (event: { latLng?: { lat: () => number; lng: () => number } }) => void) => void;
66
+ };
67
+
68
+ type GoogleMarker = {
69
+ setPosition: (latLng: LatLng) => void;
70
+ getPosition: () => { lat: () => number; lng: () => number } | null | undefined;
71
+ addListener: (event: string, callback: () => void) => void;
72
+ };
73
+
74
+ type GoogleAutocomplete = {
75
+ addListener: (event: string, callback: () => void) => void;
76
+ getPlace: () => GooglePlace;
77
+ };
78
+
79
+ declare global {
80
+ interface Window {
81
+ google?: GoogleMapsNamespace;
82
+ }
83
+ }
84
+
85
+ let googleMapsPromise: Promise<GoogleMapsNamespace> | null = null;
86
+
87
+ function isCustomGoogleMaps(element: Element): element is HTMLElement {
88
+ return element instanceof HTMLElement && element.matches(GOOGLE_MAPS_SELECTOR);
89
+ }
90
+
91
+ function parseSettings(root: HTMLElement): GoogleMapsSettings {
92
+ const raw = root.getAttribute('data-formie-custom-google-maps-settings');
93
+
94
+ if (!raw) {
95
+ return {};
96
+ }
97
+
98
+ try {
99
+ return JSON.parse(raw) as GoogleMapsSettings;
100
+ } catch {
101
+ return {};
102
+ }
103
+ }
104
+
105
+ function findElements(root: HTMLElement): GoogleMapsElements {
106
+ const inputs: Record<string, HTMLInputElement> = {};
107
+
108
+ root.querySelectorAll<HTMLInputElement>('[data-formie-custom-google-maps-field]').forEach((input) => {
109
+ const key = input.getAttribute('data-formie-custom-google-maps-field');
110
+
111
+ if (key) {
112
+ inputs[key] = input;
113
+ }
114
+ });
115
+
116
+ return {
117
+ root,
118
+ canvas: root.querySelector<HTMLElement>('[data-formie-custom-google-maps-canvas]'),
119
+ searchInput: root.querySelector<HTMLInputElement>('[data-formie-custom-google-maps-search]'),
120
+ currentLocationButton: root.querySelector<HTMLButtonElement>('[data-formie-custom-google-maps-current-location]'),
121
+ inputs,
122
+ };
123
+ }
124
+
125
+ function toNumber(value: string | number | null | undefined): number | null {
126
+ if (value === null || value === undefined || value === '') {
127
+ return null;
128
+ }
129
+
130
+ const number = Number(value);
131
+
132
+ return Number.isFinite(number) ? number : null;
133
+ }
134
+
135
+ function getCurrentLatLng(elements: GoogleMapsElements, settings: GoogleMapsSettings): LatLng {
136
+ return {
137
+ lat: toNumber(elements.inputs.lat?.value) ?? settings.defaultLat ?? -37.7841813,
138
+ lng: toNumber(elements.inputs.lng?.value) ?? settings.defaultLng ?? 144.9378721,
139
+ };
140
+ }
141
+
142
+ function getCurrentZoom(elements: GoogleMapsElements, settings: GoogleMapsSettings): number {
143
+ return toNumber(elements.inputs.zoom?.value) ?? settings.defaultZoom ?? 11;
144
+ }
145
+
146
+ function setValue(input: HTMLInputElement | undefined | null, value: string | number | null | undefined): void {
147
+ if (!input) {
148
+ return;
149
+ }
150
+
151
+ input.value = value === null || value === undefined ? '' : String(value);
152
+ input.dispatchEvent(new Event('input', { bubbles: true }));
153
+ input.dispatchEvent(new Event('change', { bubbles: true }));
154
+ }
155
+
156
+ function loadScript(src: string): Promise<void> {
157
+ return new Promise((resolve, reject) => {
158
+ const existing = Array.from(document.scripts).find((script) => script.src === src);
159
+
160
+ if (window.google?.maps?.places) {
161
+ resolve();
162
+ return;
163
+ }
164
+
165
+ if (existing) {
166
+ existing.addEventListener('load', () => resolve(), { once: true });
167
+ existing.addEventListener('error', () => reject(new Error(`Failed to load ${src}`)), { once: true });
168
+ return;
169
+ }
170
+
171
+ const script = document.createElement('script');
172
+ script.src = src;
173
+ script.async = true;
174
+ script.onload = () => resolve();
175
+ script.onerror = () => reject(new Error(`Failed to load ${src}`));
176
+ document.head.appendChild(script);
177
+ });
178
+ }
179
+
180
+ function wait(ms: number): Promise<void> {
181
+ return new Promise((resolve) => {
182
+ window.setTimeout(resolve, ms);
183
+ });
184
+ }
185
+
186
+ async function ensureGoogleMapsLibraries(timeoutMs = 12000, intervalMs = 50): Promise<GoogleMapsNamespace> {
187
+ const started = Date.now();
188
+
189
+ while (Date.now() - started < timeoutMs) {
190
+ const maps = window.google?.maps;
191
+
192
+ if (maps?.importLibrary) {
193
+ try {
194
+ if (!maps.Map) {
195
+ await maps.importLibrary('maps');
196
+ }
197
+
198
+ if (!maps.Marker) {
199
+ await maps.importLibrary('marker');
200
+ }
201
+
202
+ if (!maps.places?.Autocomplete) {
203
+ await maps.importLibrary('places');
204
+ }
205
+ } catch {
206
+ // Keep polling; the classic constructors may still arrive after the script settles.
207
+ }
208
+ }
209
+
210
+ if (window.google?.maps?.Map && window.google.maps.Marker && window.google.maps.places?.Autocomplete) {
211
+ return window.google;
212
+ }
213
+
214
+ await wait(intervalMs);
215
+ }
216
+
217
+ throw new Error('Google Maps Places API did not initialize.');
218
+ }
219
+
220
+ function loadGoogleMaps(settings: GoogleMapsSettings): Promise<GoogleMapsNamespace> {
221
+ if (!settings.apiUrl) {
222
+ return Promise.reject(new Error('Google Maps API URL is missing.'));
223
+ }
224
+
225
+ if (!googleMapsPromise) {
226
+ googleMapsPromise = loadScript(settings.apiUrl).then(() => ensureGoogleMapsLibraries());
227
+ }
228
+
229
+ return googleMapsPromise;
230
+ }
231
+
232
+ function component(components: GoogleAddressComponent[] | undefined, type: string, short = false): string {
233
+ const match = components?.find((entry) => entry.types.includes(type));
234
+
235
+ return short ? match?.short_name || '' : match?.long_name || '';
236
+ }
237
+
238
+ function parsePlace(place: GooglePlace): Record<string, string> {
239
+ const components = place.address_components || [];
240
+ const streetNumber = component(components, 'street_number');
241
+ const route = component(components, 'route');
242
+ const street1 = [streetNumber, route].filter(Boolean).join(' ');
243
+ const lat = place.geometry?.location?.lat();
244
+ const lng = place.geometry?.location?.lng();
245
+
246
+ return {
247
+ formatted: place.formatted_address || '',
248
+ raw: JSON.stringify(place),
249
+ name: place.name || '',
250
+ street1,
251
+ city: component(components, 'locality') || component(components, 'postal_town') || component(components, 'administrative_area_level_2'),
252
+ state: component(components, 'administrative_area_level_1', true),
253
+ zip: component(components, 'postal_code'),
254
+ neighborhood: component(components, 'neighborhood') || component(components, 'sublocality'),
255
+ county: component(components, 'administrative_area_level_2'),
256
+ country: component(components, 'country'),
257
+ countryCode: component(components, 'country', true),
258
+ placeId: place.place_id || '',
259
+ lat: lat === undefined ? '' : String(lat),
260
+ lng: lng === undefined ? '' : String(lng),
261
+ };
262
+ }
263
+
264
+ function applyValues(elements: GoogleMapsElements, values: Record<string, string | number>, map?: GoogleMap, marker?: GoogleMarker): void {
265
+ Object.entries(values).forEach(([key, value]) => {
266
+ setValue(elements.inputs[key], value);
267
+ });
268
+
269
+ if (values.formatted !== undefined) {
270
+ setValue(elements.searchInput, values.formatted);
271
+ }
272
+
273
+ const lat = toNumber(values.lat);
274
+ const lng = toNumber(values.lng);
275
+
276
+ if (lat !== null && lng !== null) {
277
+ const latLng = { lat, lng };
278
+ marker?.setPosition(latLng);
279
+ map?.setCenter(latLng);
280
+ }
281
+ }
282
+
283
+ async function initGoogleMapsField(elements: GoogleMapsElements, settings: GoogleMapsSettings): Promise<() => void> {
284
+ const google = await loadGoogleMaps(settings);
285
+ const initialLatLng = getCurrentLatLng(elements, settings);
286
+ const initialZoom = getCurrentZoom(elements, settings);
287
+ let map: GoogleMap | undefined;
288
+ let marker: GoogleMarker | undefined;
289
+
290
+ if (elements.canvas) {
291
+ map = new google.maps.Map(elements.canvas, {
292
+ center: initialLatLng,
293
+ zoom: initialZoom,
294
+ minZoom: settings.minZoom ?? undefined,
295
+ maxZoom: settings.maxZoom ?? undefined,
296
+ });
297
+
298
+ marker = new google.maps.Marker({
299
+ position: initialLatLng,
300
+ map,
301
+ draggable: true,
302
+ });
303
+
304
+ map.addListener('click', (event) => {
305
+ const lat = event.latLng?.lat();
306
+ const lng = event.latLng?.lng();
307
+
308
+ if (lat === undefined || lng === undefined) {
309
+ return;
310
+ }
311
+
312
+ applyValues(elements, {
313
+ lat,
314
+ lng,
315
+ zoom: map?.getZoom() || initialZoom,
316
+ }, map, marker);
317
+ });
318
+
319
+ marker.addListener('dragend', () => {
320
+ const position = marker?.getPosition();
321
+
322
+ if (!position) {
323
+ return;
324
+ }
325
+
326
+ applyValues(elements, {
327
+ lat: position.lat(),
328
+ lng: position.lng(),
329
+ zoom: map?.getZoom() || initialZoom,
330
+ }, map, marker);
331
+ });
332
+ }
333
+
334
+ let autocomplete: GoogleAutocomplete | null = null;
335
+
336
+ if (elements.searchInput) {
337
+ autocomplete = new google.maps.places.Autocomplete(elements.searchInput, {
338
+ fields: ['address_components', 'formatted_address', 'geometry', 'name', 'place_id'],
339
+ componentRestrictions: settings.country ? { country: settings.country.toLowerCase() } : undefined,
340
+ });
341
+
342
+ autocomplete.addListener('place_changed', () => {
343
+ const place = autocomplete?.getPlace();
344
+
345
+ if (!place) {
346
+ return;
347
+ }
348
+
349
+ applyValues(elements, parsePlace(place), map, marker);
350
+ });
351
+ }
352
+
353
+ const onCurrentLocationClick = (): void => {
354
+ navigator.geolocation?.getCurrentPosition((position) => {
355
+ applyValues(elements, {
356
+ lat: position.coords.latitude,
357
+ lng: position.coords.longitude,
358
+ zoom: map?.getZoom() || initialZoom,
359
+ }, map, marker);
360
+ });
361
+ };
362
+
363
+ elements.currentLocationButton?.addEventListener('click', onCurrentLocationClick);
364
+
365
+ return () => {
366
+ elements.currentLocationButton?.removeEventListener('click', onCurrentLocationClick);
367
+ };
368
+ }
369
+
370
+ function initCustomGoogleMaps(root: HTMLElement): () => void {
371
+ const elements = findElements(root);
372
+ const settings = parseSettings(root);
373
+ let cleanup: (() => void) | null = null;
374
+ let destroyed = false;
375
+
376
+ void initGoogleMapsField(elements, settings).then((destroy) => {
377
+ if (destroyed) {
378
+ destroy();
379
+ return;
380
+ }
381
+
382
+ cleanup = destroy;
383
+ }).catch((error: unknown) => {
384
+ console.warn('[Formie] Unable to initialize Google Maps custom field.', error);
385
+ });
386
+
387
+ return () => {
388
+ destroyed = true;
389
+ cleanup?.();
390
+ };
391
+ }
392
+
393
+ export const customGoogleMapsModule: FormieModuleDefinition = {
394
+ id: MODULE_ID,
395
+ kind: 'field',
396
+ match: ({ target }) => {
397
+ return target instanceof Element && (
398
+ target.matches(GOOGLE_MAPS_SELECTOR) ||
399
+ !!target.querySelector(GOOGLE_MAPS_SELECTOR)
400
+ );
401
+ },
402
+ setup: async(ctx) => {
403
+ const field = getModuleFieldTarget(ctx);
404
+ const root = field || ctx.target;
405
+
406
+ if (!(root instanceof Element)) {
407
+ return;
408
+ }
409
+
410
+ const cleanup = observeMatchingElements(root, GOOGLE_MAPS_SELECTOR, isCustomGoogleMaps, initCustomGoogleMaps);
411
+
412
+ return {
413
+ destroy: cleanup,
414
+ };
415
+ },
416
+ };
@@ -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
+ };