@coherent.js/forms 1.1.0 → 2.0.0-rc.0
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/README.md +162 -243
- package/dist/csrf.js +57 -0
- package/dist/csrf.js.map +7 -0
- package/dist/form-builder.js +483 -27
- package/dist/form-builder.js.map +3 -3
- package/dist/form-hydration.js +317 -222
- package/dist/form-hydration.js.map +3 -3
- package/dist/index.js +675 -555
- package/dist/index.js.map +4 -4
- package/dist/validation.js +376 -61
- package/dist/validation.js.map +3 -3
- package/dist/validators.js +388 -294
- package/dist/validators.js.map +4 -4
- package/package.json +6 -3
- package/types/csrf.d.ts +45 -0
- package/types/index.d.ts +185 -41
package/dist/form-hydration.js
CHANGED
|
@@ -1,197 +1,249 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return options.message || `Must be at least ${min} characters`;
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
},
|
|
25
|
-
maxLength: (value, options = {}) => {
|
|
26
|
-
if (!value) return null;
|
|
27
|
-
const max = options.max || Infinity;
|
|
28
|
-
if (value.length > max) {
|
|
29
|
-
return options.message || `Must be no more than ${max} characters`;
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
},
|
|
33
|
-
min: (value, options = {}) => {
|
|
34
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
35
|
-
const num = Number(value);
|
|
36
|
-
const minValue = options.min || 0;
|
|
37
|
-
if (isNaN(num) || num < minValue) {
|
|
38
|
-
return options.message || `Must be at least ${minValue}`;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
},
|
|
42
|
-
max: (value, options = {}) => {
|
|
43
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
44
|
-
const num = Number(value);
|
|
45
|
-
const maxValue = options.max || Infinity;
|
|
46
|
-
if (isNaN(num) || num > maxValue) {
|
|
47
|
-
return options.message || `Must be no more than ${maxValue}`;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
},
|
|
51
|
-
pattern: (value, options = {}) => {
|
|
52
|
-
if (!value) return null;
|
|
53
|
-
const regex = options.pattern || options.regex;
|
|
54
|
-
if (regex && !regex.test(value)) {
|
|
55
|
-
return options.message || "Invalid format";
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
},
|
|
59
|
-
url: (value) => {
|
|
60
|
-
if (!value) return null;
|
|
1
|
+
// src/patterns.js
|
|
2
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
|
|
3
|
+
var EMAIL_MAX_LENGTH = 254;
|
|
4
|
+
function isEmailShaped(value) {
|
|
5
|
+
return typeof value === "string" && value.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/rules.js
|
|
9
|
+
var BUILTIN = /* @__PURE__ */ Symbol("coherent.forms.builtin");
|
|
10
|
+
var DESCRIPTORS = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
var HELPER_NAMES = /* @__PURE__ */ new Set(["get", "compose", "debounce", "cancellable", "when", "chain"]);
|
|
12
|
+
var isEmpty = (value) => value === null || value === void 0 || value === "";
|
|
13
|
+
var isOptionsObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof RegExp);
|
|
14
|
+
function argsFromString(params, kind, text) {
|
|
15
|
+
if (text === void 0 || text.trim() === "") return [];
|
|
16
|
+
if (params.length === 0) return [text.trim()];
|
|
17
|
+
if (kind === "list") {
|
|
18
|
+
return [text.split(",").map((part) => part.trim()).filter(Boolean)];
|
|
19
|
+
}
|
|
20
|
+
if (kind === "regexp") {
|
|
61
21
|
try {
|
|
62
|
-
new
|
|
63
|
-
return null;
|
|
22
|
+
return [new RegExp(text)];
|
|
64
23
|
} catch {
|
|
65
|
-
return "Please enter a valid URL";
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
number: (value) => {
|
|
69
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
70
|
-
if (isNaN(Number(value))) {
|
|
71
|
-
return "Must be a valid number";
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
},
|
|
75
|
-
integer: (value) => {
|
|
76
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
77
|
-
const num = Number(value);
|
|
78
|
-
if (isNaN(num) || !Number.isInteger(num)) {
|
|
79
|
-
return "Must be a whole number";
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
},
|
|
83
|
-
phone: (value) => {
|
|
84
|
-
if (!value) return null;
|
|
85
|
-
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
86
|
-
if (!phoneRegex.test(value) || value.replace(/\D/g, "").length < 10) {
|
|
87
|
-
return "Please enter a valid phone number";
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
},
|
|
91
|
-
date: (value) => {
|
|
92
|
-
if (!value) return null;
|
|
93
|
-
const date = new Date(value);
|
|
94
|
-
if (isNaN(date.getTime())) {
|
|
95
|
-
return "Please enter a valid date";
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
},
|
|
99
|
-
match: (value, options = {}, translator, allValues = {}) => {
|
|
100
|
-
if (!value) return null;
|
|
101
|
-
const fieldName = options.field || options.fieldName;
|
|
102
|
-
if (value !== allValues[fieldName]) {
|
|
103
|
-
return options.message || `Must match ${fieldName}`;
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
},
|
|
107
|
-
custom: (value, options = {}, translator, allValues) => {
|
|
108
|
-
const validatorFn = options.validator || options.fn;
|
|
109
|
-
if (!validatorFn) return null;
|
|
110
|
-
const isValid = validatorFn(value, allValues);
|
|
111
|
-
return isValid ? null : options.message || "Validation failed";
|
|
112
|
-
},
|
|
113
|
-
fileType: (value, options = {}) => {
|
|
114
|
-
if (!value) return null;
|
|
115
|
-
const allowedTypes = options.accept || options.types || [];
|
|
116
|
-
if (value.type !== void 0) {
|
|
117
|
-
const fileType = value.type;
|
|
118
|
-
const fileExt = value.name ? value.name.split(".").pop().toLowerCase() : "";
|
|
119
|
-
const isValid = allowedTypes.some((type) => {
|
|
120
|
-
if (type.startsWith(".")) {
|
|
121
|
-
return fileExt === type.slice(1).toLowerCase();
|
|
122
|
-
}
|
|
123
|
-
if (type.includes("/")) {
|
|
124
|
-
if (type.endsWith("/*")) {
|
|
125
|
-
return fileType.startsWith(type.replace("/*", "/"));
|
|
126
|
-
}
|
|
127
|
-
return fileType === type;
|
|
128
|
-
}
|
|
129
|
-
return fileExt === type.toLowerCase();
|
|
130
|
-
});
|
|
131
|
-
if (!isValid) {
|
|
132
|
-
return options.message || `File type must be one of: ${allowedTypes.join(", ")}`;
|
|
133
|
-
}
|
|
134
24
|
return null;
|
|
135
25
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
26
|
+
}
|
|
27
|
+
const comma = text.indexOf(",");
|
|
28
|
+
const raw = (comma === -1 ? text : text.slice(0, comma)).trim();
|
|
29
|
+
const message = comma === -1 ? "" : text.slice(comma + 1).trim();
|
|
30
|
+
let arg = raw;
|
|
31
|
+
if (kind === "number") {
|
|
32
|
+
arg = Number(raw);
|
|
33
|
+
if (raw === "" || Number.isNaN(arg)) return null;
|
|
34
|
+
}
|
|
35
|
+
return message ? [arg, message] : [arg];
|
|
36
|
+
}
|
|
37
|
+
function defineRule(name, { params = [], stringArg = "string", valid, message }) {
|
|
38
|
+
const fromOptions = (options = {}) => {
|
|
39
|
+
const config = { message: options.message };
|
|
40
|
+
for (const [key, ...aliases] of params) {
|
|
41
|
+
config[key] = [key, ...aliases].map((alias) => options[alias]).find((v) => v !== void 0);
|
|
147
42
|
}
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
43
|
+
return config;
|
|
44
|
+
};
|
|
45
|
+
const check = (value, config, formData) => {
|
|
46
|
+
if (valid(value, config, formData || {})) return null;
|
|
47
|
+
return config.message || builtin.message || message(config);
|
|
48
|
+
};
|
|
49
|
+
const factory = (...args) => {
|
|
50
|
+
const config = { message: args[params.length] };
|
|
51
|
+
params.forEach(([key], index) => {
|
|
52
|
+
config[key] = args[index];
|
|
157
53
|
});
|
|
158
|
-
|
|
159
|
-
|
|
54
|
+
const rule = (value, formData, _translator, allValues) => check(value, config, allValues ?? formData);
|
|
55
|
+
DESCRIPTORS.set(rule, { name, args });
|
|
56
|
+
return rule;
|
|
57
|
+
};
|
|
58
|
+
const isDirectCall = (args) => {
|
|
59
|
+
if (args.length >= 2) {
|
|
60
|
+
return params.length === 0 || isOptionsObject(args[1]);
|
|
160
61
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!value) return null;
|
|
165
|
-
const alphaRegex = /^[a-zA-Z]+$/;
|
|
166
|
-
if (!alphaRegex.test(value)) {
|
|
167
|
-
return "Must contain only letters";
|
|
62
|
+
if (args.length === 1 && params.length === 0) {
|
|
63
|
+
const [arg] = args;
|
|
64
|
+
return arg === "" || arg !== void 0 && arg !== null && typeof arg !== "string";
|
|
168
65
|
}
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return "Must contain only letters and numbers";
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
function builtin(...args) {
|
|
69
|
+
if (isDirectCall(args)) {
|
|
70
|
+
const [value, options, , allValues] = args;
|
|
71
|
+
return check(value, fromOptions(isOptionsObject(options) ? options : {}), allValues);
|
|
176
72
|
}
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
73
|
+
return factory(...args);
|
|
74
|
+
}
|
|
75
|
+
Object.defineProperty(builtin, "name", { value: name });
|
|
76
|
+
builtin[BUILTIN] = {
|
|
77
|
+
name,
|
|
78
|
+
factory,
|
|
79
|
+
argsFromString: (text) => argsFromString(params, stringArg, text)
|
|
80
|
+
};
|
|
81
|
+
return builtin;
|
|
82
|
+
}
|
|
83
|
+
var toNumber = (value) => typeof value === "number" ? value : Number(value);
|
|
84
|
+
function testRegExp(regex, value) {
|
|
85
|
+
if (!(regex instanceof RegExp)) return true;
|
|
86
|
+
regex.lastIndex = 0;
|
|
87
|
+
return regex.test(String(value));
|
|
88
|
+
}
|
|
89
|
+
function fileMatchesType(file, allowedTypes) {
|
|
90
|
+
const fileType = file.type;
|
|
91
|
+
const fileExt = file.name ? file.name.split(".").pop().toLowerCase() : "";
|
|
92
|
+
return allowedTypes.some((type) => {
|
|
93
|
+
if (type.startsWith(".")) {
|
|
94
|
+
return fileExt === type.slice(1).toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
if (type.includes("/")) {
|
|
97
|
+
if (type.endsWith("/*")) {
|
|
98
|
+
return fileType.startsWith(type.replace("/*", "/"));
|
|
99
|
+
}
|
|
100
|
+
return fileType === type;
|
|
183
101
|
}
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
102
|
+
return fileExt === type.toLowerCase();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
var validators = {
|
|
106
|
+
required: defineRule("required", {
|
|
107
|
+
valid: (value) => !isEmpty(value),
|
|
108
|
+
message: () => "This field is required"
|
|
109
|
+
}),
|
|
110
|
+
email: defineRule("email", {
|
|
111
|
+
valid: (value) => isEmpty(value) || isEmailShaped(value),
|
|
112
|
+
message: () => "Invalid email address"
|
|
113
|
+
}),
|
|
114
|
+
url: defineRule("url", {
|
|
115
|
+
valid: (value) => {
|
|
116
|
+
if (isEmpty(value)) return true;
|
|
117
|
+
try {
|
|
118
|
+
new URL(value);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
message: () => "Invalid URL"
|
|
125
|
+
}),
|
|
126
|
+
minLength: defineRule("minLength", {
|
|
127
|
+
params: [["min", "minLength"]],
|
|
128
|
+
stringArg: "number",
|
|
129
|
+
valid: (value, { min }) => !value || !(value.length < (min ?? 0)),
|
|
130
|
+
message: ({ min }) => `Minimum length is ${min}`
|
|
131
|
+
}),
|
|
132
|
+
maxLength: defineRule("maxLength", {
|
|
133
|
+
params: [["max", "maxLength"]],
|
|
134
|
+
stringArg: "number",
|
|
135
|
+
valid: (value, { max }) => !value || !(value.length > (max ?? Infinity)),
|
|
136
|
+
message: ({ max }) => `Maximum length is ${max}`
|
|
137
|
+
}),
|
|
138
|
+
// Empty values pass (combine with `required`); a non-numeric value fails.
|
|
139
|
+
min: defineRule("min", {
|
|
140
|
+
params: [["min"]],
|
|
141
|
+
stringArg: "number",
|
|
142
|
+
valid: (value, { min }) => {
|
|
143
|
+
if (isEmpty(value)) return true;
|
|
144
|
+
const number = toNumber(value);
|
|
145
|
+
return !Number.isNaN(number) && !(number < (min ?? -Infinity));
|
|
146
|
+
},
|
|
147
|
+
message: ({ min }) => `Minimum value is ${min}`
|
|
148
|
+
}),
|
|
149
|
+
max: defineRule("max", {
|
|
150
|
+
params: [["max"]],
|
|
151
|
+
stringArg: "number",
|
|
152
|
+
valid: (value, { max }) => {
|
|
153
|
+
if (isEmpty(value)) return true;
|
|
154
|
+
const number = toNumber(value);
|
|
155
|
+
return !Number.isNaN(number) && !(number > (max ?? Infinity));
|
|
156
|
+
},
|
|
157
|
+
message: ({ max }) => `Maximum value is ${max}`
|
|
158
|
+
}),
|
|
159
|
+
pattern: defineRule("pattern", {
|
|
160
|
+
params: [["pattern", "regex"]],
|
|
161
|
+
stringArg: "regexp",
|
|
162
|
+
valid: (value, { pattern }) => isEmpty(value) || testRegExp(pattern, value),
|
|
163
|
+
message: () => "Invalid format"
|
|
164
|
+
}),
|
|
165
|
+
/** Equal to another field, even when empty */
|
|
166
|
+
matches: defineRule("matches", {
|
|
167
|
+
params: [["field", "fieldName"]],
|
|
168
|
+
valid: (value, { field }, formData) => value === formData[field],
|
|
169
|
+
message: () => "Fields do not match"
|
|
170
|
+
}),
|
|
171
|
+
/** Equal to another field; an empty value passes */
|
|
172
|
+
match: defineRule("match", {
|
|
173
|
+
params: [["field", "fieldName"]],
|
|
174
|
+
valid: (value, { field }, formData) => !value || value === formData[field],
|
|
175
|
+
message: ({ field }) => `Must match ${field}`
|
|
176
|
+
}),
|
|
177
|
+
oneOf: defineRule("oneOf", {
|
|
178
|
+
params: [["options", "values"]],
|
|
179
|
+
stringArg: "list",
|
|
180
|
+
valid: (value, { options }) => !value || !Array.isArray(options) || options.includes(value),
|
|
181
|
+
message: () => "Invalid option"
|
|
182
|
+
}),
|
|
183
|
+
custom: defineRule("custom", {
|
|
184
|
+
params: [["validator", "fn"]],
|
|
185
|
+
valid: (value, { validator }, formData) => typeof validator !== "function" || Boolean(validator(value, formData)),
|
|
186
|
+
message: () => "Validation failed"
|
|
187
|
+
}),
|
|
188
|
+
number: defineRule("number", {
|
|
189
|
+
valid: (value) => isEmpty(value) || !Number.isNaN(Number(value)),
|
|
190
|
+
message: () => "Must be a valid number"
|
|
191
|
+
}),
|
|
192
|
+
integer: defineRule("integer", {
|
|
193
|
+
valid: (value) => isEmpty(value) || Number.isInteger(Number(value)),
|
|
194
|
+
message: () => "Must be a whole number"
|
|
195
|
+
}),
|
|
196
|
+
phone: defineRule("phone", {
|
|
197
|
+
valid: (value) => !value || /^[\d\s\-+()]+$/.test(value) && value.replace(/\D/g, "").length >= 10,
|
|
198
|
+
message: () => "Please enter a valid phone number"
|
|
199
|
+
}),
|
|
200
|
+
date: defineRule("date", {
|
|
201
|
+
valid: (value) => !value || !Number.isNaN(new Date(value).getTime()),
|
|
202
|
+
message: () => "Please enter a valid date"
|
|
203
|
+
}),
|
|
204
|
+
alpha: defineRule("alpha", {
|
|
205
|
+
valid: (value) => !value || /^[a-zA-Z]+$/.test(value),
|
|
206
|
+
message: () => "Must contain only letters"
|
|
207
|
+
}),
|
|
208
|
+
alphanumeric: defineRule("alphanumeric", {
|
|
209
|
+
valid: (value) => !value || /^[a-zA-Z0-9]+$/.test(value),
|
|
210
|
+
message: () => "Must contain only letters and numbers"
|
|
211
|
+
}),
|
|
212
|
+
uppercase: defineRule("uppercase", {
|
|
213
|
+
valid: (value) => !value || value === String(value).toUpperCase(),
|
|
214
|
+
message: () => "Must be uppercase"
|
|
215
|
+
}),
|
|
216
|
+
fileType: defineRule("fileType", {
|
|
217
|
+
params: [["accept", "types"]],
|
|
218
|
+
stringArg: "list",
|
|
219
|
+
valid: (value, { accept }) => !value || value.type === void 0 || fileMatchesType(value, accept || []),
|
|
220
|
+
message: ({ accept }) => `File type must be one of: ${(accept || []).join(", ")}`
|
|
221
|
+
}),
|
|
222
|
+
fileSize: defineRule("fileSize", {
|
|
223
|
+
params: [["maxSize"]],
|
|
224
|
+
stringArg: "number",
|
|
225
|
+
valid: (value, { maxSize }) => !value || value.size === void 0 || !(value.size > (maxSize ?? Infinity)),
|
|
226
|
+
message: ({ maxSize }) => `File size must be less than ${((maxSize ?? Infinity) / (1024 * 1024)).toFixed(2)}MB`
|
|
227
|
+
}),
|
|
228
|
+
fileExtension: defineRule("fileExtension", {
|
|
229
|
+
params: [["extensions"]],
|
|
230
|
+
stringArg: "list",
|
|
231
|
+
valid: (value, { extensions }) => {
|
|
232
|
+
if (!value) return true;
|
|
233
|
+
const fileName = value.name || value;
|
|
234
|
+
const ext = `.${String(fileName).split(".").pop().toLowerCase()}`;
|
|
235
|
+
return (extensions || []).some((allowed) => ext === allowed.toLowerCase());
|
|
236
|
+
},
|
|
237
|
+
message: ({ extensions }) => `File extension must be one of: ${(extensions || []).join(", ")}`
|
|
238
|
+
}),
|
|
239
|
+
/** A registered validator or built-in by name */
|
|
240
|
+
get: (name) => Object.hasOwn(validators, name) ? validators[name] : void 0,
|
|
241
|
+
/** Combine validators into one returning the first error */
|
|
191
242
|
compose: (validatorList) => {
|
|
243
|
+
const rules = validatorList.map(resolveValidator);
|
|
192
244
|
return (value, options, translator, allValues) => {
|
|
193
|
-
for (const
|
|
194
|
-
const error =
|
|
245
|
+
for (const rule of rules) {
|
|
246
|
+
const error = rule ? rule(value, options, translator, allValues) : null;
|
|
195
247
|
if (error) {
|
|
196
248
|
return error;
|
|
197
249
|
}
|
|
@@ -199,7 +251,7 @@ var validators = {
|
|
|
199
251
|
return null;
|
|
200
252
|
};
|
|
201
253
|
},
|
|
202
|
-
|
|
254
|
+
/** Debounce an async validator */
|
|
203
255
|
debounce: (validator, delay = 300) => {
|
|
204
256
|
let timeoutId;
|
|
205
257
|
return (value) => {
|
|
@@ -212,7 +264,7 @@ var validators = {
|
|
|
212
264
|
});
|
|
213
265
|
};
|
|
214
266
|
},
|
|
215
|
-
|
|
267
|
+
/** Cancellable async validator */
|
|
216
268
|
cancellable: (validator) => {
|
|
217
269
|
let abortController;
|
|
218
270
|
const wrapped = async (value) => {
|
|
@@ -236,38 +288,31 @@ var validators = {
|
|
|
236
288
|
};
|
|
237
289
|
return wrapped;
|
|
238
290
|
},
|
|
239
|
-
|
|
291
|
+
/** Run `validator` only when `condition` holds */
|
|
240
292
|
when: (condition, validator) => {
|
|
293
|
+
const rule = resolveValidator(validator);
|
|
241
294
|
return (value, options = {}, translator, allValues = {}) => {
|
|
242
295
|
const context = options.min !== void 0 || options.max !== void 0 ? allValues : options;
|
|
243
296
|
const shouldValidate = typeof condition === "function" ? condition(value, context) : condition;
|
|
244
297
|
if (!shouldValidate) {
|
|
245
298
|
return null;
|
|
246
299
|
}
|
|
247
|
-
return
|
|
300
|
+
return rule ? rule(value, options, translator, allValues) : null;
|
|
248
301
|
};
|
|
249
302
|
},
|
|
250
|
-
|
|
303
|
+
/** Validator chain builder */
|
|
251
304
|
chain: (options = {}) => {
|
|
252
305
|
const validatorList = [];
|
|
253
306
|
const stopOnFirstError = options.stopOnFirstError !== false;
|
|
307
|
+
const direct = (name) => (opts) => {
|
|
308
|
+
validatorList.push((v, o, t, a) => validators[name](v, opts || o || {}, t, a));
|
|
309
|
+
return chain;
|
|
310
|
+
};
|
|
254
311
|
const chain = {
|
|
255
|
-
required: (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
email: (opts) => {
|
|
260
|
-
validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));
|
|
261
|
-
return chain;
|
|
262
|
-
},
|
|
263
|
-
minLength: (opts) => {
|
|
264
|
-
validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));
|
|
265
|
-
return chain;
|
|
266
|
-
},
|
|
267
|
-
maxLength: (opts) => {
|
|
268
|
-
validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));
|
|
269
|
-
return chain;
|
|
270
|
-
},
|
|
312
|
+
required: direct("required"),
|
|
313
|
+
email: direct("email"),
|
|
314
|
+
minLength: direct("minLength"),
|
|
315
|
+
maxLength: direct("maxLength"),
|
|
271
316
|
custom: (fn, message) => {
|
|
272
317
|
validatorList.push((v, o, t, a) => {
|
|
273
318
|
const result = fn(v, a);
|
|
@@ -284,21 +329,82 @@ var validators = {
|
|
|
284
329
|
}
|
|
285
330
|
}
|
|
286
331
|
return null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
332
|
+
}
|
|
333
|
+
const errors = [];
|
|
334
|
+
for (const validator of validatorList) {
|
|
335
|
+
const error = validator(value, opts, translator, allValues);
|
|
336
|
+
if (error) {
|
|
337
|
+
errors.push(error);
|
|
294
338
|
}
|
|
295
|
-
return errors.length > 0 ? errors : null;
|
|
296
339
|
}
|
|
340
|
+
return errors.length > 0 ? errors : null;
|
|
297
341
|
}
|
|
298
342
|
};
|
|
299
343
|
return chain;
|
|
300
344
|
}
|
|
301
345
|
};
|
|
346
|
+
function lookup(name) {
|
|
347
|
+
const found = Object.hasOwn(validators, name) && !HELPER_NAMES.has(name) ? validators[name] : null;
|
|
348
|
+
return typeof found === "function" ? found : null;
|
|
349
|
+
}
|
|
350
|
+
function resolveString(entry) {
|
|
351
|
+
const exact = lookup(entry);
|
|
352
|
+
const colon = exact ? -1 : entry.indexOf(":");
|
|
353
|
+
const name = colon === -1 ? entry : entry.slice(0, colon).trim();
|
|
354
|
+
const found = exact ?? lookup(name);
|
|
355
|
+
if (!found) return null;
|
|
356
|
+
const text = colon === -1 ? void 0 : entry.slice(colon + 1);
|
|
357
|
+
if (!found[BUILTIN]) {
|
|
358
|
+
return text === void 0 ? { name, args: [], validator: found } : null;
|
|
359
|
+
}
|
|
360
|
+
const args = found[BUILTIN].argsFromString(text);
|
|
361
|
+
if (!args) return null;
|
|
362
|
+
return { name: found[BUILTIN].name, args, validator: found[BUILTIN].factory(...args) };
|
|
363
|
+
}
|
|
364
|
+
function resolveValidator(entry) {
|
|
365
|
+
if (typeof entry === "string") {
|
|
366
|
+
return resolveString(entry)?.validator ?? null;
|
|
367
|
+
}
|
|
368
|
+
if (typeof entry !== "function") return null;
|
|
369
|
+
return entry[BUILTIN] ? entry[BUILTIN].factory() : entry;
|
|
370
|
+
}
|
|
371
|
+
function reviveArg(arg) {
|
|
372
|
+
if (isOptionsObject(arg) && Array.isArray(arg.$regexp)) {
|
|
373
|
+
try {
|
|
374
|
+
return new RegExp(arg.$regexp[0], arg.$regexp[1]);
|
|
375
|
+
} catch {
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return arg;
|
|
380
|
+
}
|
|
381
|
+
function parseValidators(attribute) {
|
|
382
|
+
if (!attribute) return [];
|
|
383
|
+
const text = String(attribute).trim();
|
|
384
|
+
let specs;
|
|
385
|
+
if (text.startsWith("[")) {
|
|
386
|
+
try {
|
|
387
|
+
specs = JSON.parse(text);
|
|
388
|
+
} catch {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
if (!Array.isArray(specs)) return [];
|
|
392
|
+
} else {
|
|
393
|
+
specs = text.split(",").map((part) => {
|
|
394
|
+
const [name, ...params] = part.trim().split(":");
|
|
395
|
+
return { name, args: params.map((p) => p !== "" && !Number.isNaN(Number(p)) ? Number(p) : p) };
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return specs.map((spec) => {
|
|
399
|
+
if (!spec || typeof spec.name !== "string") return null;
|
|
400
|
+
const { name } = spec;
|
|
401
|
+
if (!Object.hasOwn(validators, name) || HELPER_NAMES.has(name)) return null;
|
|
402
|
+
const entry = validators[name];
|
|
403
|
+
if (typeof entry !== "function") return null;
|
|
404
|
+
const args = Array.isArray(spec.args) ? spec.args.map(reviveArg) : [];
|
|
405
|
+
return entry[BUILTIN] ? entry[BUILTIN].factory(...args) : entry;
|
|
406
|
+
}).filter(Boolean);
|
|
407
|
+
}
|
|
302
408
|
|
|
303
409
|
// src/form-builder.js
|
|
304
410
|
import { render as renderToHTML } from "@coherent.js/core";
|
|
@@ -349,17 +455,6 @@ function hydrateForm(formSelector, options = {}) {
|
|
|
349
455
|
fields: /* @__PURE__ */ new Map()
|
|
350
456
|
};
|
|
351
457
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
352
|
-
function parseValidators(validatorString) {
|
|
353
|
-
if (!validatorString) return [];
|
|
354
|
-
return validatorString.split(",").map((v) => {
|
|
355
|
-
const trimmed = v.trim();
|
|
356
|
-
const [name, ...params] = trimmed.split(":");
|
|
357
|
-
if (validators[name]) {
|
|
358
|
-
return params.length > 0 ? validators[name](...params.map((p) => isNaN(p) ? p : Number(p))) : validators[name];
|
|
359
|
-
}
|
|
360
|
-
return null;
|
|
361
|
-
}).filter(Boolean);
|
|
362
|
-
}
|
|
363
458
|
function discoverFields() {
|
|
364
459
|
const inputs = form.querySelectorAll("[name]");
|
|
365
460
|
inputs.forEach((input) => {
|