@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
@@ -5,6 +5,7 @@ import { getTextLimitMetrics } from '@verbb/formie-core';
5
5
  import type { FormieModuleDefinition } from '#contracts/modules';
6
6
  import { getModuleFieldTarget, releaseFormValidators, retainFormValidators } from '#modules/fields/shared';
7
7
  import { ensureModuleStyles } from '#modules/styles';
8
+ import { t } from '#utils/i18n';
8
9
 
9
10
  const INPUT_SELECTOR = 'input[data-formie-single-line-text-input], textarea[data-formie-multi-line-text-input]';
10
11
  const TEXT_LIMIT_VALIDATORS = [
@@ -15,8 +16,17 @@ const TEXT_LIMIT_VALIDATORS = [
15
16
  ] as const;
16
17
  const VALIDATOR_SCOPE = 'text-limit';
17
18
  const ALLOW_OVERTYPE_ATTR = 'data-formie-text-limit-allow-overtype';
19
+ const TEXT_LIMIT_CHARACTERS_ALLOWED = '{count, plural, one{character allowed} other{characters allowed}}';
20
+ const TEXT_LIMIT_CHARACTERS_LEFT = '{count, plural, one{character left} other{characters left}}';
21
+ const TEXT_LIMIT_CHARACTERS_OVER = '{count, plural, one{character over limit} other{characters over limit}}';
22
+ const TEXT_LIMIT_WORDS_ALLOWED = '{count, plural, one{word allowed} other{words allowed}}';
23
+ const TEXT_LIMIT_WORDS_LEFT = '{count, plural, one{word left} other{words left}}';
24
+ const TEXT_LIMIT_WORDS_OVER = '{count, plural, one{word over limit} other{words over limit}}';
18
25
  const limitTargetCache = new WeakMap<HTMLInputElement | HTMLTextAreaElement, HTMLElement | null>();
19
26
 
27
+ type TextLimitUnit = 'character' | 'word';
28
+ type TextLimitCounterState = 'allowed' | 'left' | 'over';
29
+
20
30
  ensureModuleStyles('text-limit', [textLimitCss]);
21
31
 
22
32
  type TextLimitOptions = {
@@ -69,10 +79,11 @@ function registerValidators(form: HTMLFormElement | null): void {
69
79
 
70
80
  return getTextLimitMetrics(input.value).graphemeCount >= limit;
71
81
  }, ({ label, input, t }) => {
72
- return t('{attribute} must be no less than {min} characters.', {
73
- attribute: label,
74
- min: input.getAttribute('data-formie-min-chars') || '',
75
- });
82
+ return input.getAttribute('data-formie-validation-min-characters-message')
83
+ || t('{label} must be no less than {min} characters.', {
84
+ label,
85
+ min: input.getAttribute('data-formie-min-chars') || '',
86
+ });
76
87
  });
77
88
 
78
89
  validator.addValidator('textMaxCharacterLimit', ({ input }) => {
@@ -92,10 +103,11 @@ function registerValidators(form: HTMLFormElement | null): void {
92
103
 
93
104
  return getTextLimitMetrics(input.value).graphemeCount <= limit;
94
105
  }, ({ label, input, t }) => {
95
- return t('{attribute} must be no greater than {max} characters.', {
96
- attribute: label,
97
- max: input.getAttribute('data-formie-max-chars') || '',
98
- });
106
+ return input.getAttribute('data-formie-validation-max-characters-message')
107
+ || t('{label} must be no greater than {max} characters.', {
108
+ label,
109
+ max: input.getAttribute('data-formie-max-chars') || '',
110
+ });
99
111
  });
100
112
 
101
113
  validator.addValidator('textMinWordLimit', ({ input }) => {
@@ -111,10 +123,11 @@ function registerValidators(form: HTMLFormElement | null): void {
111
123
 
112
124
  return getTextLimitMetrics(input.value).wordCount >= limit;
113
125
  }, ({ label, input, t }) => {
114
- return t('{attribute} must be no less than {min} words.', {
115
- attribute: label,
116
- min: input.getAttribute('data-formie-min-words') || '',
117
- });
126
+ return input.getAttribute('data-formie-validation-min-words-message')
127
+ || t('{label} must be no less than {min} words.', {
128
+ label,
129
+ min: input.getAttribute('data-formie-min-words') || '',
130
+ });
118
131
  });
119
132
 
120
133
  validator.addValidator('textMaxWordLimit', ({ input }) => {
@@ -134,10 +147,11 @@ function registerValidators(form: HTMLFormElement | null): void {
134
147
 
135
148
  return getTextLimitMetrics(input.value).wordCount <= limit;
136
149
  }, ({ label, input, t }) => {
137
- return t('{attribute} must be no greater than {max} words.', {
138
- attribute: label,
139
- max: input.getAttribute('data-formie-max-words') || '',
140
- });
150
+ return input.getAttribute('data-formie-validation-max-words-message')
151
+ || t('{label} must be no greater than {max} words.', {
152
+ label,
153
+ max: input.getAttribute('data-formie-max-words') || '',
154
+ });
141
155
  });
142
156
  });
143
157
  }
@@ -182,14 +196,65 @@ function getLimitTarget(input: HTMLInputElement | HTMLTextAreaElement): HTMLElem
182
196
  return target;
183
197
  }
184
198
 
185
- function renderCounter(target: HTMLElement, remaining: number, unit: 'character' | 'word'): void {
199
+ function getTextLimitCounterState(
200
+ input: HTMLInputElement | HTMLTextAreaElement,
201
+ remaining: number,
202
+ unit: TextLimitUnit,
203
+ ): TextLimitCounterState {
204
+ const isEmpty = unit === 'character' ? input.value === '' : input.value.trim() === '';
205
+
206
+ if (isEmpty) {
207
+ return 'allowed';
208
+ }
209
+
210
+ if (remaining < 0) {
211
+ return 'over';
212
+ }
213
+
214
+ return 'left';
215
+ }
216
+
217
+ function getTextLimitCounterMessageKey(unit: TextLimitUnit, state: TextLimitCounterState): string {
218
+ if (unit === 'character') {
219
+ if (state === 'allowed') {
220
+ return TEXT_LIMIT_CHARACTERS_ALLOWED;
221
+ }
222
+
223
+ if (state === 'over') {
224
+ return TEXT_LIMIT_CHARACTERS_OVER;
225
+ }
226
+
227
+ return TEXT_LIMIT_CHARACTERS_LEFT;
228
+ }
229
+
230
+ if (state === 'allowed') {
231
+ return TEXT_LIMIT_WORDS_ALLOWED;
232
+ }
233
+
234
+ if (state === 'over') {
235
+ return TEXT_LIMIT_WORDS_OVER;
236
+ }
237
+
238
+ return TEXT_LIMIT_WORDS_LEFT;
239
+ }
240
+
241
+ function renderCounter(
242
+ target: HTMLElement,
243
+ input: HTMLInputElement | HTMLTextAreaElement,
244
+ remaining: number,
245
+ limit: number,
246
+ unit: TextLimitUnit,
247
+ ): void {
248
+ const state = getTextLimitCounterState(input, remaining, unit);
249
+ const displayCount = state === 'allowed' ? limit : Math.abs(remaining);
186
250
  const number = document.createElement('span');
187
- number.className = remaining < 0 ? 'formie-limit-number formie-limit-number-error' : 'formie-limit-number';
188
- number.textContent = String(remaining);
251
+ number.className = state === 'over' ? 'formie-limit-number formie-limit-number-error' : 'formie-limit-number';
252
+ number.textContent = String(displayCount);
253
+ const messageKey = getTextLimitCounterMessageKey(unit, state);
189
254
 
190
255
  target.replaceChildren(
191
256
  number,
192
- document.createTextNode(` ${Math.abs(remaining) === 1 ? unit : `${unit}s`} left`)
257
+ document.createTextNode(` ${t(messageKey, { count: displayCount })}`)
193
258
  );
194
259
  }
195
260
 
@@ -206,14 +271,14 @@ function updateCounter(input: HTMLInputElement | HTMLTextAreaElement): void {
206
271
 
207
272
  if (maxChars > 0) {
208
273
  const remaining = maxChars - metrics.graphemeCount;
209
- renderCounter(target, remaining, 'character');
274
+ renderCounter(target, input, remaining, maxChars, 'character');
210
275
 
211
276
  return;
212
277
  }
213
278
 
214
279
  if (maxWords > 0) {
215
280
  const remaining = maxWords - metrics.wordCount;
216
- renderCounter(target, remaining, 'word');
281
+ renderCounter(target, input, remaining, maxWords, 'word');
217
282
  }
218
283
  }
219
284
 
@@ -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,
@@ -0,0 +1,54 @@
1
+ export type CountryFromIpResponse = {
2
+ countryCode?: string | null;
3
+ countryName?: string | null;
4
+ };
5
+
6
+ const DEFAULT_COUNTRY_FROM_IP_ACTION = 'formie/address/country-from-ip';
7
+
8
+ let cachedLookup: Promise<CountryFromIpResponse | null> | null = null;
9
+
10
+ function buildActionUrl(action: string): string {
11
+ return new URL(action.startsWith('/') ? action : `/actions/${action}`, window.location.origin).toString();
12
+ }
13
+
14
+ export async function fetchCountryFromIp(
15
+ action: string = DEFAULT_COUNTRY_FROM_IP_ACTION,
16
+ ): Promise<CountryFromIpResponse | null> {
17
+ if (!cachedLookup) {
18
+ cachedLookup = (async () => {
19
+ try {
20
+ const response = await fetch(buildActionUrl(action), {
21
+ headers: {
22
+ Accept: 'application/json',
23
+ },
24
+ });
25
+
26
+ if (!response.ok) {
27
+ return null;
28
+ }
29
+
30
+ const data = await response.json() as CountryFromIpResponse;
31
+
32
+ if (!data?.countryCode) {
33
+ return null;
34
+ }
35
+
36
+ return data;
37
+ } catch {
38
+ return null;
39
+ }
40
+ })();
41
+ }
42
+
43
+ return cachedLookup;
44
+ }
45
+
46
+ export function createGeoIpLookup(
47
+ action: string = DEFAULT_COUNTRY_FROM_IP_ACTION,
48
+ ): (callback: (countryCode: string) => void) => void {
49
+ return (callback) => {
50
+ void fetchCountryFromIp(action).then((data) => {
51
+ callback(data?.countryCode?.toLowerCase() || '');
52
+ });
53
+ };
54
+ }
@@ -0,0 +1,303 @@
1
+ import { fieldKeyToInputName, normalizeFieldKey } from '#utils/field-references.keys';
2
+ import { resolveFieldReferenceLive } from '#utils/field-references.resolver';
3
+ import type { FieldValueRegistry, ResolveFieldValueResult } from '#utils/field-references.types';
4
+
5
+ export type RowScopeParams = {
6
+ scope?: string;
7
+ index?: string | number;
8
+ rows?: string;
9
+ fieldKind?: string;
10
+ };
11
+
12
+ const ROW_SCOPE_VALUES = new Set(['first', 'last', 'index', 'all', 'count', 'rows']);
13
+
14
+ function escapeRegExp(value: string): string {
15
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }
17
+
18
+ export function parseRowsExpression(expression: string, rowCount: number): number[] {
19
+ const normalized = String(expression || '').trim().toLowerCase();
20
+
21
+ if (!normalized || rowCount <= 0) {
22
+ return [];
23
+ }
24
+
25
+ if (normalized === 'even') {
26
+ const indices: number[] = [];
27
+
28
+ for (let row = 1; row <= rowCount; row++) {
29
+ if (row % 2 === 0) {
30
+ indices.push(row - 1);
31
+ }
32
+ }
33
+
34
+ return indices;
35
+ }
36
+
37
+ if (normalized === 'odd') {
38
+ const indices: number[] = [];
39
+
40
+ for (let row = 1; row <= rowCount; row++) {
41
+ if (row % 2 === 1) {
42
+ indices.push(row - 1);
43
+ }
44
+ }
45
+
46
+ return indices;
47
+ }
48
+
49
+ const everyMatch = normalized.match(/^every:(\d+)$/);
50
+
51
+ if (everyMatch) {
52
+ const step = Math.max(1, Number.parseInt(everyMatch[1] || '1', 10));
53
+ const indices: number[] = [];
54
+
55
+ for (let row = 1; row <= rowCount; row += step) {
56
+ indices.push(row - 1);
57
+ }
58
+
59
+ return indices;
60
+ }
61
+
62
+ const indices: number[] = [];
63
+
64
+ normalized.split(/\s*,\s*/).forEach((segment) => {
65
+ const trimmed = segment.trim();
66
+
67
+ if (!trimmed) {
68
+ return;
69
+ }
70
+
71
+ const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
72
+
73
+ if (rangeMatch) {
74
+ let start = Number.parseInt(rangeMatch[1] || '0', 10);
75
+ let end = Number.parseInt(rangeMatch[2] || '0', 10);
76
+
77
+ if (start > end) {
78
+ [start, end] = [end, start];
79
+ }
80
+
81
+ for (let row = start; row <= end; row++) {
82
+ if (row >= 1 && row <= rowCount) {
83
+ indices.push(row - 1);
84
+ }
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ const row = Number.parseInt(trimmed, 10);
91
+
92
+ if (Number.isFinite(row) && row >= 1 && row <= rowCount) {
93
+ indices.push(row - 1);
94
+ }
95
+ });
96
+
97
+ return [...new Set(indices)].sort((left, right) => left - right);
98
+ }
99
+
100
+ function resolveColumnSelector(sourceKey: string): { fieldKey: string; columnKey: string } {
101
+ const normalized = normalizeFieldKey(sourceKey);
102
+ const parts = normalized.split('.').filter(Boolean);
103
+
104
+ if (parts.length < 2) {
105
+ return {
106
+ fieldKey: normalized,
107
+ columnKey: parts[parts.length - 1] || '',
108
+ };
109
+ }
110
+
111
+ if (parts.length >= 3 && /^\d+$/.test(parts[1] || '')) {
112
+ return {
113
+ fieldKey: parts[0] || '',
114
+ columnKey: parts.slice(2).join('.'),
115
+ };
116
+ }
117
+
118
+ return {
119
+ fieldKey: parts[0] || '',
120
+ columnKey: parts.slice(1).join('.'),
121
+ };
122
+ }
123
+
124
+ function listColumnCellKeys(fieldKey: string, columnKey: string, registry: FieldValueRegistry): string[] {
125
+ const pattern = new RegExp(`^${escapeRegExp(fieldKey)}\\.(\\d+)\\.${escapeRegExp(columnKey)}$`);
126
+
127
+ return [...registry.keys()]
128
+ .filter((key) => pattern.test(key))
129
+ .sort((left, right) => {
130
+ const leftRow = Number.parseInt(left.split('.')[1] || '0', 10);
131
+ const rightRow = Number.parseInt(right.split('.')[1] || '0', 10);
132
+
133
+ return leftRow - rightRow;
134
+ });
135
+ }
136
+
137
+ function readCellValue(key: string, registry: FieldValueRegistry): string | string[] {
138
+ return resolveFieldReferenceLive(key, registry).value;
139
+ }
140
+
141
+ export function getRowScopedWatchNames(
142
+ sourceKey: string,
143
+ params: RowScopeParams,
144
+ registry: FieldValueRegistry,
145
+ ): Set<string> {
146
+ const watchNames = new Set<string>();
147
+ const { fieldKey, columnKey } = resolveColumnSelector(sourceKey);
148
+ const scope = String(params.scope || '').trim().toLowerCase();
149
+
150
+ if (!fieldKey || !columnKey || !ROW_SCOPE_VALUES.has(scope)) {
151
+ const fallback = fieldKeyToInputName(sourceKey);
152
+
153
+ if (fallback) {
154
+ watchNames.add(fallback);
155
+ watchNames.add(`${fallback}[]`);
156
+ }
157
+
158
+ return watchNames;
159
+ }
160
+
161
+ const cellKeys = listColumnCellKeys(fieldKey, columnKey, registry);
162
+
163
+ cellKeys.forEach((cellKey) => {
164
+ const entry = registry.get(cellKey);
165
+
166
+ if (entry?.names?.length) {
167
+ entry.names.forEach((name) => {
168
+ watchNames.add(name);
169
+ });
170
+
171
+ return;
172
+ }
173
+
174
+ const fallback = fieldKeyToInputName(cellKey);
175
+
176
+ if (fallback) {
177
+ watchNames.add(fallback);
178
+ watchNames.add(`${fallback}[]`);
179
+ }
180
+ });
181
+
182
+ return watchNames;
183
+ }
184
+
185
+ export function resolveRowScopedFieldReference(
186
+ sourceKey: string,
187
+ params: RowScopeParams,
188
+ registry: FieldValueRegistry,
189
+ ): ResolveFieldValueResult {
190
+ const scope = String(params.scope || '').trim().toLowerCase();
191
+
192
+ if (!scope || !ROW_SCOPE_VALUES.has(scope)) {
193
+ return resolveFieldReferenceLive(sourceKey, registry);
194
+ }
195
+
196
+ const { fieldKey, columnKey } = resolveColumnSelector(sourceKey);
197
+
198
+ if (!fieldKey || !columnKey) {
199
+ return resolveFieldReferenceLive(sourceKey, registry);
200
+ }
201
+
202
+ const cellKeys = listColumnCellKeys(fieldKey, columnKey, registry);
203
+ const rowValues = cellKeys.map((cellKey) => readCellValue(cellKey, registry));
204
+
205
+ if (scope === 'count') {
206
+ return {
207
+ key: `${fieldKey}.${columnKey}`,
208
+ value: String(cellKeys.length),
209
+ found: true,
210
+ };
211
+ }
212
+
213
+ if (scope === 'first') {
214
+ const key = cellKeys[0] || `${fieldKey}.0.${columnKey}`;
215
+
216
+ return {
217
+ key,
218
+ value: rowValues[0] ?? '',
219
+ found: cellKeys.length > 0,
220
+ };
221
+ }
222
+
223
+ if (scope === 'last') {
224
+ const key = cellKeys[cellKeys.length - 1] || `${fieldKey}.0.${columnKey}`;
225
+
226
+ return {
227
+ key,
228
+ value: rowValues[rowValues.length - 1] ?? '',
229
+ found: cellKeys.length > 0,
230
+ };
231
+ }
232
+
233
+ if (scope === 'index') {
234
+ const index = Number.parseInt(String(params.index ?? '0'), 10);
235
+ const key = `${fieldKey}.${index}.${columnKey}`;
236
+
237
+ return resolveFieldReferenceLive(key, registry);
238
+ }
239
+
240
+ if (scope === 'all') {
241
+ const values = rowValues.flatMap((value) => {
242
+ if (Array.isArray(value)) {
243
+ return value;
244
+ }
245
+
246
+ if (value === '') {
247
+ return [];
248
+ }
249
+
250
+ return [value];
251
+ });
252
+
253
+ return {
254
+ key: `${fieldKey}.${columnKey}`,
255
+ value: values,
256
+ found: values.length > 0,
257
+ };
258
+ }
259
+
260
+ if (scope === 'rows') {
261
+ const indices = parseRowsExpression(String(params.rows || ''), cellKeys.length);
262
+
263
+ if (indices.length === 0) {
264
+ return {
265
+ key: `${fieldKey}.${columnKey}`,
266
+ value: '',
267
+ found: false,
268
+ };
269
+ }
270
+
271
+ if (indices.length === 1) {
272
+ const key = cellKeys[indices[0]] || `${fieldKey}.${indices[0]}.${columnKey}`;
273
+
274
+ return {
275
+ key,
276
+ value: rowValues[indices[0]] ?? '',
277
+ found: true,
278
+ };
279
+ }
280
+
281
+ const values = indices.flatMap((index) => {
282
+ const value = rowValues[index];
283
+
284
+ if (Array.isArray(value)) {
285
+ return value;
286
+ }
287
+
288
+ if (value === '') {
289
+ return [];
290
+ }
291
+
292
+ return [value];
293
+ });
294
+
295
+ return {
296
+ key: `${fieldKey}.${columnKey}`,
297
+ value: values,
298
+ found: values.length > 0,
299
+ };
300
+ }
301
+
302
+ return resolveFieldReferenceLive(sourceKey, registry);
303
+ }
@@ -2,6 +2,7 @@ export { normalizeFieldKey, fieldKeyToInputName, inputNameToFieldKey } from '#ut
2
2
  export { parseFieldReference } from '#utils/field-references.parser';
3
3
  export { buildFieldValueRegistry } from '#utils/field-references.registry';
4
4
  export { resolveFieldReferenceLive, resolveFieldReferenceFromFormData } from '#utils/field-references.resolver';
5
+ export { getRowScopedWatchNames, parseRowsExpression, resolveRowScopedFieldReference } from '#utils/field-references.row-scope';
5
6
  export type {
6
7
  FieldReferenceTransform,
7
8
  FieldValueRegistry,