@verbb/formie-browser 1.0.2 → 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 +16 -0
- package/dist/chunks/{address-finder-CQW81kY0.js → address-finder-BU4roxT6.js} +2 -2
- package/dist/chunks/{bpoint-D8OjLmsn.js → bpoint-uNg5j6rt.js} +1 -1
- package/dist/chunks/{calculations-BqZsENCq.js → calculations-C9t6Zop9.js} +2 -2
- package/dist/chunks/{captcha-eu-DIa6aUWz.js → captcha-eu-BAPzHNts.js} +2 -2
- package/dist/chunks/{checkbox-radio-BI-UjgMF.js → checkbox-radio-CHeLNVIK.js} +39 -34
- package/dist/chunks/{conditions-BVl6jTYo.js → conditions-DKxVCsjD.js} +189 -163
- package/dist/chunks/{date-picker-69TgvErA.js → date-picker-B3iQ797Z.js} +2 -2
- package/dist/chunks/{eway-1GPxH6DU.js → eway-DCielIML.js} +2 -2
- package/dist/chunks/file-upload-DP2TOUYV.js +316 -0
- package/dist/chunks/{friendly-captcha-v1-DA2QPKA3.js → friendly-captcha-v1-DdlcGiFY.js} +1 -1
- package/dist/chunks/{friendly-captcha-v2-BorOWiz_.js → friendly-captcha-v2-76w-YnFm.js} +1 -1
- package/dist/chunks/{go-cardless-DwBgERcA.js → go-cardless-rwCkzbNK.js} +1 -1
- package/dist/chunks/{google-address-BcTBncyS.js → google-address-BD8IZ86V.js} +77 -58
- package/dist/chunks/{hcaptcha-BchdVCqq.js → hcaptcha-BNNTL6uf.js} +2 -2
- package/dist/chunks/{hidden-D8RqSPt7.js → hidden-CW6Rcf1N.js} +1 -1
- package/dist/chunks/{index-C1ZOKiAi.js → index-D6npYxvr.js} +1372 -1178
- package/dist/chunks/{loqate-nXuTX7In.js → loqate-B-ZNS65B.js} +2 -2
- package/dist/chunks/{mollie-qdCynQCd.js → mollie-BDAZ-1X4.js} +1 -1
- package/dist/chunks/{moneris-BNEA-laO.js → moneris-B1b0uEtY.js} +1 -1
- package/dist/chunks/{opayo-BJtXOkuK.js → opayo-CC5hraHo.js} +2 -2
- package/dist/chunks/{paddle-D72Pa8kr.js → paddle-B4GmrDsq.js} +2 -2
- package/dist/chunks/{paypal-Cqt2bned.js → paypal-BFS6XsIS.js} +2 -2
- package/dist/chunks/{payway-BjCqsc0H.js → payway-DekoJead.js} +2 -2
- package/dist/chunks/{phone-country-K9pYAlzM.js → phone-country-Cm5YUeXI.js} +2 -2
- package/dist/chunks/{place-kit-Dg6f_UoM.js → place-kit-DizZWV_e.js} +1 -1
- package/dist/chunks/{recaptcha-enterprise--VYAeeqz.js → recaptcha-enterprise-BbOvO7JZ.js} +2 -2
- package/dist/chunks/{recaptcha-shared-PSIEEEe1.js → recaptcha-shared-Dwg1eVFS.js} +2 -2
- package/dist/chunks/{recaptcha-v2-checkbox-atBNe5ml.js → recaptcha-v2-checkbox-SY3ti5iQ.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-DnBufNPT.js → recaptcha-v2-invisible-DSmQnn6c.js} +2 -2
- package/dist/chunks/{recaptcha-v3-CF6O8rDj.js → recaptcha-v3-DNh7YCnR.js} +2 -2
- package/dist/chunks/{repeater-CB7Yojvx.js → repeater-DM0vuFNQ.js} +2 -2
- package/dist/chunks/{rich-text-nAV_SQZo.js → rich-text-C8aGYp70.js} +1 -1
- package/dist/chunks/{scripts-BkAAux0i.js → scripts-CKTFtxPO.js} +1 -1
- package/dist/chunks/{shared-DGqeMvcJ.js → shared--BU5CFoL.js} +4 -4
- package/dist/chunks/{signature-BT6CsQ6n.js → signature-pFhZfH7d.js} +1 -1
- package/dist/chunks/{snaptcha-C7qQo02f.js → snaptcha-VuOUOaLj.js} +1 -1
- package/dist/chunks/{square-C5AH3trb.js → square-BR9pvToy.js} +4 -4
- package/dist/chunks/{stripe-EJK0RyUv.js → stripe-OHUwoV9g.js} +2 -2
- package/dist/chunks/{summary-DC_ipXCU.js → summary-Da7Ktg3N.js} +2 -2
- package/dist/chunks/{table-BEAI99qo.js → table-C0PmR9bI.js} +2 -2
- package/dist/chunks/text-limit-mLEwZDIa.js +144 -0
- package/dist/chunks/{turnstile-_uCRwnU-.js → turnstile-9tM7q_LK.js} +2 -2
- package/dist/css/theme/forms/_form.css +28 -0
- package/dist/index.js +9 -9
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- 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/address/google-address.d.ts +7 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions/effects.d.ts +3 -1
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions.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/submit/pipeline.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/scope.d.ts +6 -0
- package/dist/js/validation/scope.d.ts.map +1 -0
- package/dist/js/validation/submit-readiness.d.ts +4 -0
- package/dist/js/validation/submit-readiness.d.ts.map +1 -0
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/css/theme/forms/_form.css +28 -0
- package/src/js/core/create-formie-client.ts +6 -0
- package/src/js/core/field-error-container.ts +39 -0
- package/src/js/core/submit-result-state.ts +29 -2
- package/src/js/core/submit-result-ui.ts +3 -9
- package/src/js/modules/address/google-address.ts +33 -1
- package/src/js/modules/fields/checkbox-radio.ts +21 -15
- package/src/js/modules/fields/conditions/effects.ts +79 -2
- package/src/js/modules/fields/conditions.ts +53 -11
- package/src/js/modules/fields/file-upload.ts +200 -105
- package/src/js/modules/fields/text-limit.ts +87 -22
- package/src/js/submit/pipeline.ts +3 -28
- 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/scope.ts +25 -0
- package/src/js/validation/submit-readiness.ts +157 -0
- package/src/js/validation/validator.ts +11 -7
- package/dist/chunks/file-upload-BDZZftgL.js +0 -281
- package/dist/chunks/text-limit-Blgq5G-G.js +0 -135
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
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function getFormPages(form: HTMLFormElement): HTMLElement[] {
|
|
2
|
+
return Array.from(form.querySelectorAll('[data-formie-page]')) as HTMLElement[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getValidationScope(form: HTMLFormElement): { scope: Element; final: boolean } {
|
|
6
|
+
const pages = getFormPages(form);
|
|
7
|
+
|
|
8
|
+
if (!pages.length) {
|
|
9
|
+
return {
|
|
10
|
+
scope: form,
|
|
11
|
+
final: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Multi-page submits validate the visible page until the user reaches the
|
|
16
|
+
// final step, where hidden earlier pages are included again.
|
|
17
|
+
const currentPage = pages.find((page) => {
|
|
18
|
+
return !page.hasAttribute('data-formie-page-hidden');
|
|
19
|
+
}) || pages[pages.length - 1];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
scope: currentPage,
|
|
23
|
+
final: currentPage === pages[pages.length - 1],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { getValidationScope } from '#validation/scope';
|
|
2
|
+
import type { FormieValidator } from '#validation/validator';
|
|
3
|
+
|
|
4
|
+
const SUBMIT_VALIDATION_DISABLED_ATTR = 'data-formie-submit-validation-disabled';
|
|
5
|
+
const PRESERVED_DISABLED_ATTR = 'data-formie-preserve-disabled';
|
|
6
|
+
const SUBMIT_READY_ATTR = 'data-formie-submit-ready';
|
|
7
|
+
|
|
8
|
+
function isSubmitReadinessEnabled(form: HTMLFormElement): boolean {
|
|
9
|
+
return form.dataset.formieDisableSubmitUntilValid === 'true';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getPrimarySubmitButtons(form: HTMLFormElement): HTMLButtonElement[] {
|
|
13
|
+
return Array.from(form.querySelectorAll('button[data-formie-action="submit"]')).filter((button): button is HTMLButtonElement => {
|
|
14
|
+
return button instanceof HTMLButtonElement;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSubmitButtonVisible(button: HTMLButtonElement): boolean {
|
|
19
|
+
return !button.hasAttribute('data-formie-conditionally-hidden')
|
|
20
|
+
&& !button.closest('[data-formie-conditionally-hidden]');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function syncSubmitReadiness(form: HTMLFormElement, validator: FormieValidator): void {
|
|
24
|
+
if (!isSubmitReadinessEnabled(form)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (form.getAttribute('data-formie-loading') === 'true') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { scope, final } = getValidationScope(form);
|
|
33
|
+
const isReady = validator.isValid(scope, {
|
|
34
|
+
includeHiddenPages: final,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
form.setAttribute(SUBMIT_READY_ATTR, isReady ? 'true' : 'false');
|
|
38
|
+
|
|
39
|
+
getPrimarySubmitButtons(form).forEach((button) => {
|
|
40
|
+
if (!isSubmitButtonVisible(button)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isReady) {
|
|
45
|
+
if (!button.hasAttribute(SUBMIT_VALIDATION_DISABLED_ATTR)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (button.hasAttribute(PRESERVED_DISABLED_ATTR)) {
|
|
50
|
+
button.disabled = true;
|
|
51
|
+
button.removeAttribute(PRESERVED_DISABLED_ATTR);
|
|
52
|
+
} else {
|
|
53
|
+
button.disabled = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button.removeAttribute(SUBMIT_VALIDATION_DISABLED_ATTR);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!button.hasAttribute(SUBMIT_VALIDATION_DISABLED_ATTR)) {
|
|
61
|
+
if (button.disabled) {
|
|
62
|
+
button.setAttribute(PRESERVED_DISABLED_ATTR, 'true');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button.setAttribute(SUBMIT_VALIDATION_DISABLED_ATTR, 'true');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
button.disabled = true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function bindSubmitReadiness(
|
|
73
|
+
form: HTMLFormElement,
|
|
74
|
+
validator: FormieValidator,
|
|
75
|
+
target: Element,
|
|
76
|
+
): () => void {
|
|
77
|
+
if (!isSubmitReadinessEnabled(form)) {
|
|
78
|
+
return () => {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let syncQueued = false;
|
|
82
|
+
|
|
83
|
+
const scheduleSync = (): void => {
|
|
84
|
+
if (syncQueued) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
syncQueued = true;
|
|
89
|
+
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
syncQueued = false;
|
|
92
|
+
syncSubmitReadiness(form, validator);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
scheduleSync();
|
|
97
|
+
|
|
98
|
+
const inputHandler = (): void => {
|
|
99
|
+
scheduleSync();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
form.addEventListener('input', inputHandler, true);
|
|
103
|
+
form.addEventListener('change', inputHandler, true);
|
|
104
|
+
|
|
105
|
+
const resetHandler = (): void => {
|
|
106
|
+
window.setTimeout(() => {
|
|
107
|
+
scheduleSync();
|
|
108
|
+
}, 0);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
form.addEventListener('reset', resetHandler);
|
|
112
|
+
|
|
113
|
+
const conditionsHandler = (): void => {
|
|
114
|
+
scheduleSync();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
target.addEventListener('formie:conditions:evaluated', conditionsHandler as EventListener);
|
|
118
|
+
|
|
119
|
+
const observer = new MutationObserver((mutations) => {
|
|
120
|
+
const shouldSync = mutations.some((mutation) => {
|
|
121
|
+
if (mutation.type === 'attributes') {
|
|
122
|
+
const attributeName = mutation.attributeName || '';
|
|
123
|
+
|
|
124
|
+
return attributeName === 'data-formie-page-hidden'
|
|
125
|
+
|| attributeName === 'data-formie-conditionally-hidden'
|
|
126
|
+
|| attributeName === 'data-formie-loading'
|
|
127
|
+
|| attributeName === 'disabled';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return mutation.type === 'childList';
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (shouldSync) {
|
|
134
|
+
scheduleSync();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
observer.observe(form, {
|
|
139
|
+
childList: true,
|
|
140
|
+
subtree: true,
|
|
141
|
+
attributes: true,
|
|
142
|
+
attributeFilter: [
|
|
143
|
+
'data-formie-page-hidden',
|
|
144
|
+
'data-formie-conditionally-hidden',
|
|
145
|
+
'data-formie-loading',
|
|
146
|
+
'disabled',
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
form.removeEventListener('input', inputHandler, true);
|
|
152
|
+
form.removeEventListener('change', inputHandler, true);
|
|
153
|
+
form.removeEventListener('reset', resetHandler);
|
|
154
|
+
target.removeEventListener('formie:conditions:evaluated', conditionsHandler as EventListener);
|
|
155
|
+
observer.disconnect();
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureFieldErrorContainer } from '#core/field-error-container';
|
|
1
2
|
import { syncPageTabErrors } from '#core/page-tab-errors';
|
|
2
3
|
import rules from '#validation/rules';
|
|
3
4
|
import type {
|
|
@@ -176,6 +177,10 @@ export class FormieValidator {
|
|
|
176
177
|
return this.submitted || this.activated.has(input);
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
isValid(inputOrSelector: Element | null = null, options: ValidateOptions = {}): boolean {
|
|
181
|
+
return this.validate(inputOrSelector, options).length === 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
validate(inputOrSelector: Element | null = null, options: ValidateOptions = {}): ValidationError[] {
|
|
180
185
|
this.errors = [];
|
|
181
186
|
|
|
@@ -316,12 +321,11 @@ export class FormieValidator {
|
|
|
316
321
|
let errorMessages = fieldContainer.querySelector('[data-formie-field-errors]') as HTMLElement | null;
|
|
317
322
|
|
|
318
323
|
if (!errorMessages) {
|
|
319
|
-
errorMessages =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
fieldContainer.appendChild(errorMessages);
|
|
324
|
+
errorMessages = ensureFieldErrorContainer(fieldContainer, (container) => {
|
|
325
|
+
if (this.config.messagesClass.length) {
|
|
326
|
+
container.classList.add(...this.config.messagesClass);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
if (this.config.messagesClass.length) {
|
|
@@ -398,7 +402,7 @@ export class FormieValidator {
|
|
|
398
402
|
|
|
399
403
|
getErrorMessage(input: ValidationInput, validatorName: string, validator: ValidatorDefinition, opts: ValidationContext): string {
|
|
400
404
|
const errorMessage = typeof validator.errorMessage === 'function' ? validator.errorMessage(opts) : validator.errorMessage;
|
|
401
|
-
return errorMessage ?? t('{
|
|
405
|
+
return errorMessage ?? t('{label} is invalid.', { label: opts.label });
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
getErrors(): ValidationError[] {
|