@verbb/formie-browser 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/chunks/{address-finder-BjMuEiwF.js → address-finder-BU4roxT6.js} +2 -2
- package/dist/chunks/{bpoint-KudsBCo2.js → bpoint-uNg5j6rt.js} +1 -1
- package/dist/chunks/{calculations-DlGSkQ3W.js → calculations-C9t6Zop9.js} +2 -2
- package/dist/chunks/{captcha-eu-CterYFok.js → captcha-eu-BAPzHNts.js} +2 -2
- package/dist/chunks/{checkbox-radio-CBvgk3hR.js → checkbox-radio-CHeLNVIK.js} +39 -34
- package/dist/chunks/{conditions-DYy1-Hwt.js → conditions-DKxVCsjD.js} +2 -2
- package/dist/chunks/{date-picker-BlD--zfe.js → date-picker-B3iQ797Z.js} +2 -2
- package/dist/chunks/{eway-qwc-B5Us.js → eway-DCielIML.js} +2 -2
- package/dist/chunks/{file-upload-CIgHrXfp.js → file-upload-DP2TOUYV.js} +34 -34
- package/dist/chunks/{friendly-captcha-v1-EDFac33c.js → friendly-captcha-v1-DdlcGiFY.js} +1 -1
- package/dist/chunks/{friendly-captcha-v2-BDpSvFGR.js → friendly-captcha-v2-76w-YnFm.js} +1 -1
- package/dist/chunks/{go-cardless-DuDMVsnz.js → go-cardless-rwCkzbNK.js} +1 -1
- package/dist/chunks/{google-address-Bmjp01pH.js → google-address-BD8IZ86V.js} +2 -2
- package/dist/chunks/{hcaptcha-BTQ6An6g.js → hcaptcha-BNNTL6uf.js} +2 -2
- package/dist/chunks/{hidden-DbPdOxKb.js → hidden-CW6Rcf1N.js} +1 -1
- package/dist/chunks/{index-BcqwpPNE.js → index-D6npYxvr.js} +790 -681
- package/dist/chunks/{loqate-BdNGv93o.js → loqate-B-ZNS65B.js} +2 -2
- package/dist/chunks/{mollie-DFOjTuGX.js → mollie-BDAZ-1X4.js} +1 -1
- package/dist/chunks/{moneris-BbFfl-EL.js → moneris-B1b0uEtY.js} +1 -1
- package/dist/chunks/{opayo-CmgYa9ya.js → opayo-CC5hraHo.js} +2 -2
- package/dist/chunks/{paddle-BUi7w1_o.js → paddle-B4GmrDsq.js} +2 -2
- package/dist/chunks/{paypal-A-ePfh6k.js → paypal-BFS6XsIS.js} +2 -2
- package/dist/chunks/{payway-BqFCbhyo.js → payway-DekoJead.js} +2 -2
- package/dist/chunks/{phone-country-DPMiqk4L.js → phone-country-Cm5YUeXI.js} +2 -2
- package/dist/chunks/{place-kit-CX7tamAg.js → place-kit-DizZWV_e.js} +1 -1
- package/dist/chunks/{recaptcha-enterprise-DxF6cW95.js → recaptcha-enterprise-BbOvO7JZ.js} +2 -2
- package/dist/chunks/{recaptcha-shared-DhGcbiaq.js → recaptcha-shared-Dwg1eVFS.js} +2 -2
- package/dist/chunks/{recaptcha-v2-checkbox-R9EfbZ4k.js → recaptcha-v2-checkbox-SY3ti5iQ.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-DV9hswAp.js → recaptcha-v2-invisible-DSmQnn6c.js} +2 -2
- package/dist/chunks/{recaptcha-v3-V0xc-Edh.js → recaptcha-v3-DNh7YCnR.js} +2 -2
- package/dist/chunks/{repeater-DKUT6TGI.js → repeater-DM0vuFNQ.js} +2 -2
- package/dist/chunks/{rich-text-SH7KiaKH.js → rich-text-C8aGYp70.js} +1 -1
- package/dist/chunks/{scripts-D9N2d5rN.js → scripts-CKTFtxPO.js} +1 -1
- package/dist/chunks/{shared-BmntPmnW.js → shared--BU5CFoL.js} +1 -1
- package/dist/chunks/{signature-BA3tQsGT.js → signature-pFhZfH7d.js} +1 -1
- package/dist/chunks/{snaptcha-DV7p6J9k.js → snaptcha-VuOUOaLj.js} +1 -1
- package/dist/chunks/{square-Bs-ndqYK.js → square-BR9pvToy.js} +4 -4
- package/dist/chunks/{stripe-DkZAHxtn.js → stripe-OHUwoV9g.js} +2 -2
- package/dist/chunks/{summary-Dq2i9TQk.js → summary-Da7Ktg3N.js} +2 -2
- package/dist/chunks/{table-Cu4WfVcw.js → table-C0PmR9bI.js} +2 -2
- package/dist/chunks/text-limit-mLEwZDIa.js +144 -0
- package/dist/chunks/{turnstile-CId90tRr.js → turnstile-9tM7q_LK.js} +2 -2
- package/dist/index.js +9 -9
- package/dist/js/core/field-error-container.d.ts +4 -0
- package/dist/js/core/field-error-container.d.ts.map +1 -0
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
- package/dist/js/utils/i18n.d.ts.map +1 -1
- package/dist/js/validation/rules/email.d.ts.map +1 -1
- package/dist/js/validation/rules/number.d.ts.map +1 -1
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/js/core/field-error-container.ts +39 -0
- package/src/js/core/submit-result-state.ts +12 -2
- package/src/js/core/submit-result-ui.ts +3 -9
- package/src/js/modules/fields/checkbox-radio.ts +21 -15
- package/src/js/modules/fields/file-upload.ts +12 -9
- package/src/js/modules/fields/text-limit.ts +87 -22
- package/src/js/utils/i18n.ts +184 -5
- package/src/js/validation/rules/email.ts +3 -2
- package/src/js/validation/rules/match.ts +2 -2
- package/src/js/validation/rules/minmax.ts +4 -4
- package/src/js/validation/rules/number.ts +9 -5
- package/src/js/validation/rules/pattern.ts +4 -4
- package/src/js/validation/rules/required.ts +1 -1
- package/src/js/validation/rules/url.ts +1 -1
- package/src/js/validation/validator.ts +7 -7
- package/dist/chunks/text-limit-2ycmB-Ae.js +0 -135
|
@@ -102,6 +102,16 @@ function getConfiguredSubmitAction(form: HTMLFormElement): string {
|
|
|
102
102
|
return (form.dataset.formieSubmitAction || '').trim();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function getResolvedSubmitAction(form: HTMLFormElement, result: FormSubmitResult): string {
|
|
106
|
+
const fromResponse = result.meta?.effectiveSubmitAction;
|
|
107
|
+
|
|
108
|
+
if (typeof fromResponse === 'string' && fromResponse.trim() !== '') {
|
|
109
|
+
return fromResponse.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return getConfiguredSubmitAction(form);
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function shouldHideFormOnSuccess(form: HTMLFormElement): boolean {
|
|
106
116
|
const rawValue = form.dataset.formieSubmitActionFormHide;
|
|
107
117
|
|
|
@@ -580,7 +590,7 @@ export function applySubmitResultState(form: HTMLFormElement, result: FormSubmit
|
|
|
580
590
|
}
|
|
581
591
|
|
|
582
592
|
if (action === 'submit' && !result.redirect?.url) {
|
|
583
|
-
const configuredSubmitAction =
|
|
593
|
+
const configuredSubmitAction = getResolvedSubmitAction(form, result);
|
|
584
594
|
const preserveHiddenState = configuredSubmitAction === 'message' && shouldHideFormOnSuccess(form);
|
|
585
595
|
|
|
586
596
|
if (configuredSubmitAction === 'reload') {
|
|
@@ -603,7 +613,7 @@ export function applySubmitResultState(form: HTMLFormElement, result: FormSubmit
|
|
|
603
613
|
}
|
|
604
614
|
|
|
605
615
|
if (action === 'submit' && result.redirect?.url && result.redirect.target === 'new-tab') {
|
|
606
|
-
const configuredSubmitAction =
|
|
616
|
+
const configuredSubmitAction = getResolvedSubmitAction(form, result);
|
|
607
617
|
const preserveHiddenState = configuredSubmitAction === 'message' && shouldHideFormOnSuccess(form);
|
|
608
618
|
clearPendingFinalSubmitReset(form);
|
|
609
619
|
resetSubmissionState(form, { preserveHiddenState });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FormSubmitResult } from '#contracts/schema';
|
|
2
|
+
import { ensureFieldErrorContainer as resolveFieldErrorContainer } from '#core/field-error-container';
|
|
2
3
|
import { syncPageTabErrors } from '#core/page-tab-errors';
|
|
3
4
|
import { setFormHiddenState } from '#core/submit-result-state';
|
|
4
5
|
import { addThemeClasses, removeThemeClasses } from '#theme/theme-classes';
|
|
@@ -150,16 +151,9 @@ function ensureFormSuccessContainer(form: HTMLFormElement, position: string): HT
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function ensureFieldErrorContainer(fieldNode: Element): HTMLElement {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!container) {
|
|
156
|
-
container = document.createElement('div');
|
|
157
|
-
container.setAttribute('data-formie-field-errors', 'true');
|
|
154
|
+
return resolveFieldErrorContainer(fieldNode, (container) => {
|
|
158
155
|
addThemeClasses(container, fieldNode, 'fieldErrors');
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return container;
|
|
156
|
+
});
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
function removeDescribedBy(input: HTMLElement, describedById: string): void {
|
|
@@ -31,6 +31,14 @@ function getMinMaxRule(getRule: (rule: string) => unknown): { min: number | null
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getSelectedOptionCount(field: HTMLElement): number {
|
|
35
|
+
return Array.from(field.querySelectorAll('input[type="checkbox"]')).filter((input): input is HTMLInputElement => {
|
|
36
|
+
return input instanceof HTMLInputElement && !isToggleCheckbox(input);
|
|
37
|
+
}).filter((input) => {
|
|
38
|
+
return input.checked;
|
|
39
|
+
}).length;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function registerValidators(form: HTMLFormElement | null): void {
|
|
35
43
|
// Several checkbox/radio module instances can exist in one form, but the
|
|
36
44
|
// custom validator should only be registered once per form lifecycle.
|
|
@@ -42,12 +50,7 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
42
50
|
return true;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
const
|
|
46
|
-
return input instanceof HTMLInputElement && !isToggleCheckbox(input);
|
|
47
|
-
});
|
|
48
|
-
const selected = inputs.filter((input) => {
|
|
49
|
-
return input.checked;
|
|
50
|
-
}).length;
|
|
53
|
+
const selected = getSelectedOptionCount(field);
|
|
51
54
|
const { min, max } = getMinMaxRule(getRule);
|
|
52
55
|
|
|
53
56
|
if (min !== null && selected < min) {
|
|
@@ -61,21 +64,24 @@ function registerValidators(form: HTMLFormElement | null): void {
|
|
|
61
64
|
return true;
|
|
62
65
|
},
|
|
63
66
|
({ field, label, t, getRule }) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (min !== null && max !== null) {
|
|
67
|
-
return t('{attribute} must select between {min} and {max}.', { attribute: label, min, max });
|
|
67
|
+
if (!field) {
|
|
68
|
+
return t('{label} has an invalid value.', { label });
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const selected = getSelectedOptionCount(field);
|
|
72
|
+
const { min, max } = getMinMaxRule(getRule);
|
|
73
|
+
|
|
74
|
+
if (min !== null && selected < min) {
|
|
75
|
+
return field.getAttribute('data-formie-validation-min-options-message')
|
|
76
|
+
?? t('{label} should contain at least {min, number} {min, plural, one{option} other{options}}.', { label, min });
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
if (max !== null) {
|
|
75
|
-
return
|
|
79
|
+
if (max !== null && selected > max) {
|
|
80
|
+
return field.getAttribute('data-formie-validation-max-options-message')
|
|
81
|
+
?? t('{label} should contain at most {max, number} {max, plural, one{option} other{options}}.', { label, max });
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
return t('{
|
|
84
|
+
return t('{label} has an invalid value.', { label });
|
|
79
85
|
},
|
|
80
86
|
);
|
|
81
87
|
});
|
|
@@ -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
|
|
243
|
-
|
|
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
|
|
260
|
-
|
|
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
|
|
277
|
-
|
|
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,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
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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 =
|
|
188
|
-
number.textContent = String(
|
|
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(` ${
|
|
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
|
|
package/src/js/utils/i18n.ts
CHANGED
|
@@ -79,18 +79,197 @@ export function mergeFormieTranslations(translations: Record<string, string>): R
|
|
|
79
79
|
return getFormieTranslations();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
function parsePluralBranches(body: string): Record<string, string> {
|
|
83
|
+
const branches: Record<string, string> = {};
|
|
84
|
+
let index = 0;
|
|
85
|
+
|
|
86
|
+
while (index < body.length) {
|
|
87
|
+
while (index < body.length && /\s/.test(body[index])) {
|
|
88
|
+
index++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (index >= body.length) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const branchMatch = body.slice(index).match(/^(\w+|=\d+)\{/);
|
|
96
|
+
|
|
97
|
+
if (!branchMatch) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const key = branchMatch[1];
|
|
102
|
+
index += branchMatch[0].length;
|
|
103
|
+
|
|
104
|
+
let depth = 1;
|
|
105
|
+
const contentStart = index;
|
|
106
|
+
|
|
107
|
+
while (index < body.length && depth > 0) {
|
|
108
|
+
if (body[index] === '{') {
|
|
109
|
+
depth++;
|
|
110
|
+
} else if (body[index] === '}') {
|
|
111
|
+
depth--;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (depth > 0) {
|
|
115
|
+
index++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
branches[key] = body.slice(contentStart, index);
|
|
120
|
+
index++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return branches;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function selectPluralBranch(count: number, branches: Record<string, string>): string {
|
|
127
|
+
const exactKey = `=${count}`;
|
|
128
|
+
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(branches, exactKey)) {
|
|
130
|
+
return branches[exactKey];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof Intl !== 'undefined' && typeof Intl.PluralRules === 'function') {
|
|
134
|
+
const category = new Intl.PluralRules().select(count);
|
|
135
|
+
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(branches, category)) {
|
|
137
|
+
return branches[category];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (count === 1 && Object.prototype.hasOwnProperty.call(branches, 'one')) {
|
|
142
|
+
return branches.one;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Object.prototype.hasOwnProperty.call(branches, 'other')) {
|
|
146
|
+
return branches.other;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const firstKey = Object.keys(branches)[0];
|
|
150
|
+
|
|
151
|
+
return firstKey ? branches[firstKey] : '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parsePluralExpression(message: string, startIndex: number): { param: string; body: string; endIndex: number } | null {
|
|
155
|
+
const headerMatch = message.slice(startIndex).match(/^\{(\w+),\s*plural,\s*/);
|
|
156
|
+
|
|
157
|
+
if (!headerMatch) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const param = headerMatch[1];
|
|
162
|
+
const bodyStart = startIndex + headerMatch[0].length;
|
|
163
|
+
let index = bodyStart;
|
|
164
|
+
|
|
165
|
+
while (index < message.length) {
|
|
166
|
+
while (index < message.length && /\s/.test(message[index])) {
|
|
167
|
+
index++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (index >= message.length || message[index] === '}') {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const branchMatch = message.slice(index).match(/^(\w+|=\d+)\{/);
|
|
84
175
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
176
|
+
if (!branchMatch) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
index += branchMatch[0].length;
|
|
181
|
+
|
|
182
|
+
let depth = 1;
|
|
183
|
+
|
|
184
|
+
while (index < message.length && depth > 0) {
|
|
185
|
+
if (message[index] === '{') {
|
|
186
|
+
depth++;
|
|
187
|
+
} else if (message[index] === '}') {
|
|
188
|
+
depth--;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (depth > 0) {
|
|
192
|
+
index++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
index++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (index >= message.length || message[index] !== '}') {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
param,
|
|
205
|
+
body: message.slice(bodyStart, index),
|
|
206
|
+
endIndex: index,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatPluralBlocks(message: string, replacements: TranslationReplacements): string {
|
|
211
|
+
let output = '';
|
|
212
|
+
let index = 0;
|
|
213
|
+
|
|
214
|
+
while (index < message.length) {
|
|
215
|
+
if (message[index] !== '{') {
|
|
216
|
+
output += message[index];
|
|
217
|
+
index++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const parsed = parsePluralExpression(message, index);
|
|
222
|
+
|
|
223
|
+
if (!parsed) {
|
|
224
|
+
output += message[index];
|
|
225
|
+
index++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const rawValue = replacements[parsed.param];
|
|
230
|
+
const count = typeof rawValue === 'number'
|
|
231
|
+
? rawValue
|
|
232
|
+
: Number.parseInt(String(rawValue ?? ''), 10) || 0;
|
|
233
|
+
const branches = parsePluralBranches(parsed.body);
|
|
234
|
+
let selected = selectPluralBranch(count, branches);
|
|
235
|
+
|
|
236
|
+
selected = selected.replace(/#/g, String(count));
|
|
237
|
+
|
|
238
|
+
output += selected;
|
|
239
|
+
index = parsed.endIndex + 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return output;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function formatNumberBlocks(message: string, replacements: TranslationReplacements): string {
|
|
246
|
+
return message.replace(/\{(\w+),\s*number\}/g, (match, key) => {
|
|
247
|
+
if (!Object.prototype.hasOwnProperty.call(replacements, key)) {
|
|
248
|
+
return match;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const value = replacements[key];
|
|
252
|
+
|
|
253
|
+
return typeof value === 'number' ? value.toLocaleString() : String(value);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function replaceSimpleTokens(message: string, replacements: TranslationReplacements): string {
|
|
258
|
+
return message.replace(/\{(\w+)\}/g, (match, key) => {
|
|
88
259
|
if (Object.prototype.hasOwnProperty.call(replacements, key)) {
|
|
89
260
|
return String(replacements[key]);
|
|
90
261
|
}
|
|
91
262
|
|
|
92
263
|
return match;
|
|
93
264
|
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function t(message: string, replacements: TranslationReplacements = {}): string {
|
|
268
|
+
let output = getTranslationStore()[message] || message;
|
|
269
|
+
|
|
270
|
+
output = formatPluralBlocks(output, replacements);
|
|
271
|
+
output = formatNumberBlocks(output, replacements);
|
|
272
|
+
output = replaceSimpleTokens(output, replacements);
|
|
94
273
|
|
|
95
274
|
return output;
|
|
96
275
|
}
|
|
@@ -9,9 +9,10 @@ const email: ValidationRuleDefinition = {
|
|
|
9
9
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value);
|
|
10
10
|
},
|
|
11
11
|
message: ({ input, label, t }) => {
|
|
12
|
-
return input.getAttribute('data-formie-
|
|
12
|
+
return input.getAttribute('data-formie-validation-email-message')
|
|
13
|
+
?? input.getAttribute('data-formie-pattern-email-message')
|
|
13
14
|
?? input.getAttribute('data-pattern-email-message')
|
|
14
|
-
?? t('{
|
|
15
|
+
?? t('{label} is not a valid email address.', { label });
|
|
15
16
|
},
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -16,8 +16,8 @@ const match: ValidationRuleDefinition = {
|
|
|
16
16
|
const sourceField = sourceInput?.closest('[data-formie-field-handle]') as HTMLElement | null;
|
|
17
17
|
const sourceLabel = getLabelText(sourceField);
|
|
18
18
|
|
|
19
|
-
return ctx.t('{
|
|
20
|
-
|
|
19
|
+
return ctx.t('{label} must match {value}.', {
|
|
20
|
+
label: ctx.label,
|
|
21
21
|
value: sourceLabel,
|
|
22
22
|
});
|
|
23
23
|
},
|
|
@@ -29,18 +29,18 @@ const minmax: ValidationRuleDefinition = {
|
|
|
29
29
|
const max = input.hasAttribute('max') ? parseFloat(input.getAttribute('max') || '') : null;
|
|
30
30
|
|
|
31
31
|
if (min !== null && max !== null) {
|
|
32
|
-
return t('{
|
|
32
|
+
return t('{label} must be between {min} and {max}.', { label, min, max });
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (min !== null) {
|
|
36
|
-
return t('{
|
|
36
|
+
return t('{label} must be no less than {min}.', { label, min });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (max !== null) {
|
|
40
|
-
return t('{
|
|
40
|
+
return t('{label} must be no greater than {max}.', { label, max });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
return t('{
|
|
43
|
+
return t('{label} has an invalid value.', { label });
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -35,20 +35,24 @@ const number: ValidationRuleDefinition = {
|
|
|
35
35
|
const max = rule !== true && rule && typeof rule === 'object' && typeof rule.max === 'number' ? rule.max : null;
|
|
36
36
|
|
|
37
37
|
if (min !== null && max !== null) {
|
|
38
|
-
return
|
|
38
|
+
return input.getAttribute('data-formie-validation-number-min-message')
|
|
39
|
+
?? t('{label} must be between {min} and {max}.', { label, min, max });
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
if (min !== null) {
|
|
42
|
-
return
|
|
43
|
+
return input.getAttribute('data-formie-validation-number-min-message')
|
|
44
|
+
?? t('{label} must be no less than {min}.', { label, min });
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
if (max !== null) {
|
|
46
|
-
return
|
|
48
|
+
return input.getAttribute('data-formie-validation-number-max-message')
|
|
49
|
+
?? t('{label} must be no greater than {max}.', { label, max });
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
return input.getAttribute('data-formie-
|
|
52
|
+
return input.getAttribute('data-formie-validation-number-message')
|
|
53
|
+
?? input.getAttribute('data-formie-pattern-number-message')
|
|
50
54
|
?? input.getAttribute('data-pattern-number-message')
|
|
51
|
-
?? t('{
|
|
55
|
+
?? t('{label} is not a valid number.', { label });
|
|
52
56
|
},
|
|
53
57
|
};
|
|
54
58
|
|
|
@@ -13,10 +13,10 @@ const pattern: ValidationRuleDefinition = {
|
|
|
13
13
|
},
|
|
14
14
|
message: ({ input, label, t }) => {
|
|
15
15
|
const messages = {
|
|
16
|
-
email: t('{
|
|
17
|
-
url: t('{
|
|
18
|
-
number: t('{
|
|
19
|
-
default: t('{
|
|
16
|
+
email: t('{label} is not a valid email address.', { label }),
|
|
17
|
+
url: t('{label} is not a valid URL.', { label }),
|
|
18
|
+
number: t('{label} is not a valid number.', { label }),
|
|
19
|
+
default: t('{label} is not a valid format.', { label }),
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
return input.getAttribute(`data-formie-pattern-${input.type}-message`)
|
|
@@ -23,7 +23,7 @@ const required: ValidationRuleDefinition = {
|
|
|
23
23
|
message: ({ input, label, t }) => {
|
|
24
24
|
return input.getAttribute('data-formie-required-message')
|
|
25
25
|
?? input.getAttribute('data-required-message')
|
|
26
|
-
?? t('{
|
|
26
|
+
?? t('{label} cannot be blank.', { label });
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -16,7 +16,7 @@ const url: ValidationRuleDefinition = {
|
|
|
16
16
|
message: ({ input, label, t }) => {
|
|
17
17
|
return input.getAttribute('data-formie-pattern-url-message')
|
|
18
18
|
?? input.getAttribute('data-pattern-url-message')
|
|
19
|
-
?? t('{
|
|
19
|
+
?? t('{label} is not a valid URL.', { label });
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
|