actual-css 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/actual.css +4929 -0
- package/dist/actual.js +3 -0
- package/dist/actual.js.map +31 -0
- package/dist/actual.min.css +1 -0
- package/docs/README.md +26 -0
- package/docs/components.md +860 -0
- package/docs/design-notes/accessibility-decisions.md +32 -0
- package/docs/design-notes/browser-support.md +93 -0
- package/docs/design-notes/cascade-layer.md +52 -0
- package/docs/design-notes/css-anchor.md +33 -0
- package/docs/design-notes/naming.md +37 -0
- package/docs/design-notes/previous-work.md +5 -0
- package/docs/design-notes/references.md +324 -0
- package/docs/forms.md +1265 -0
- package/docs/javascript.md +426 -0
- package/docs/layout.md +480 -0
- package/docs/mental-model.md +73 -0
- package/docs/patterns.md +415 -0
- package/docs/reserved.md +7 -0
- package/docs/tokens.md +455 -0
- package/docs/typography.md +159 -0
- package/docs/ui.md +937 -0
- package/docs/utilities.md +384 -0
- package/llms.txt +17 -0
- package/package.json +101 -0
- package/src/css/actual.css +17 -0
- package/src/css/actual.full.css +2 -0
- package/src/css/actual.layer.css +3 -0
- package/src/css/base.css +70 -0
- package/src/css/components/accordion.css +97 -0
- package/src/css/components/actions.css +21 -0
- package/src/css/components/alert.css +137 -0
- package/src/css/components/avatar.css +199 -0
- package/src/css/components/badge.css +143 -0
- package/src/css/components/breadcrumb.css +56 -0
- package/src/css/components/busy.css +44 -0
- package/src/css/components/button.css +216 -0
- package/src/css/components/card.css +140 -0
- package/src/css/components/drawer.css +122 -0
- package/src/css/components/flyout.css +243 -0
- package/src/css/components/index.css +41 -0
- package/src/css/components/join.css +106 -0
- package/src/css/components/key.css +25 -0
- package/src/css/components/meter.css +81 -0
- package/src/css/components/modal.css +162 -0
- package/src/css/components/navbar.css +98 -0
- package/src/css/components/overline.css +49 -0
- package/src/css/components/pagination.css +30 -0
- package/src/css/components/progress.css +126 -0
- package/src/css/components/scrollspy.css +27 -0
- package/src/css/components/skeleton.css +67 -0
- package/src/css/components/spinner.css +60 -0
- package/src/css/components/status-bar.css +82 -0
- package/src/css/components/surface.css +48 -0
- package/src/css/components/tab.css +128 -0
- package/src/css/components/table.css +64 -0
- package/src/css/components/tooltip.css +107 -0
- package/src/css/dialog-fallback.css +56 -0
- package/src/css/focus.css +13 -0
- package/src/css/forms/choice-card.css +158 -0
- package/src/css/forms/choice.css +196 -0
- package/src/css/forms/control.css +106 -0
- package/src/css/forms/custom-select.css +93 -0
- package/src/css/forms/form-actions.css +43 -0
- package/src/css/forms/form.css +57 -0
- package/src/css/forms/index.css +15 -0
- package/src/css/forms/input-icon.css +36 -0
- package/src/css/forms/native.css +68 -0
- package/src/css/forms/select.css +36 -0
- package/src/css/forms/switch.css +127 -0
- package/src/css/forms/validation.css +63 -0
- package/src/css/grid.css +95 -0
- package/src/css/intents.css +33 -0
- package/src/css/layout.css +80 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +55 -0
- package/src/css/optional/scroll-snap.css +51 -0
- package/src/css/optional/scroller.css +51 -0
- package/src/css/optional/typography-fluid.css +48 -0
- package/src/css/optional/utilities-extra.css +289 -0
- package/src/css/print.css +47 -0
- package/src/css/prose.css +187 -0
- package/src/css/reset.css +56 -0
- package/src/css/theme.css +186 -0
- package/src/css/themes/corporate.css +60 -0
- package/src/css/themes/cyberpunk.css +60 -0
- package/src/css/themes/dark.css +32 -0
- package/src/css/themes/dim.css +33 -0
- package/src/css/themes/edge.css +143 -0
- package/src/css/themes/forest.css +65 -0
- package/src/css/themes/gradient.css +97 -0
- package/src/css/themes/index.css +43 -0
- package/src/css/themes/lavender.css +66 -0
- package/src/css/themes/mono.css +60 -0
- package/src/css/themes/neon.css +66 -0
- package/src/css/themes/ocean.css +60 -0
- package/src/css/themes/petrol.css +64 -0
- package/src/css/themes/spruce.css +64 -0
- package/src/css/themes/square.css +68 -0
- package/src/css/themes/sunset.css +66 -0
- package/src/css/tokens.css +122 -0
- package/src/css/utilities.css +162 -0
- package/src/css/variants.css +118 -0
- package/src/js/command.js +165 -0
- package/src/js/context-menu.js +291 -0
- package/src/js/dialog-fallback.js +163 -0
- package/src/js/dialog.js +503 -0
- package/src/js/enhance.js +208 -0
- package/src/js/events.js +22 -0
- package/src/js/filter.js +122 -0
- package/src/js/floating.js +462 -0
- package/src/js/flyout.js +187 -0
- package/src/js/focus.js +35 -0
- package/src/js/index.js +26 -0
- package/src/js/input.js +47 -0
- package/src/js/keys.js +30 -0
- package/src/js/mask.js +184 -0
- package/src/js/menu.js +88 -0
- package/src/js/password.js +98 -0
- package/src/js/scrollspy.js +121 -0
- package/src/js/selectors.js +59 -0
- package/src/js/status.js +158 -0
- package/src/js/surface.js +358 -0
- package/src/js/tab.js +173 -0
- package/src/js/tooltip.js +374 -0
- package/src/js/validation.js +419 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Validation — optional form validation enhancer.
|
|
3
|
+
*
|
|
4
|
+
* Native HTML constraint validation runs first. This enhancer prevents
|
|
5
|
+
* premature error display, adds invalid state on submit, focuses the first
|
|
6
|
+
* invalid field, and supports a few small custom rules through
|
|
7
|
+
* data-validation-rules. It is a progressive enhancement, not a validation
|
|
8
|
+
* framework: server / AJAX validation lives in app code and feeds results
|
|
9
|
+
* back through FormValidator.setErrors().
|
|
10
|
+
*
|
|
11
|
+
* Opt in with the .needs-validation class. Importing the module registers
|
|
12
|
+
* the behavior; there is no init call.
|
|
13
|
+
*
|
|
14
|
+
* The .needs-validation class is a static, init-time contract: this module
|
|
15
|
+
* does not observe attribute mutations, so toggling the class after connect
|
|
16
|
+
* only takes effect on the next focusout/input event, not immediately. A
|
|
17
|
+
* submit that races that window can still bypass native validation (the
|
|
18
|
+
* managed novalidate attribute is still present). Treat the class as fixed
|
|
19
|
+
* for a form's lifetime rather than something toggled at runtime. Removing
|
|
20
|
+
* the form from the DOM, however, is fully supported: the enhance() sweep
|
|
21
|
+
* runs the cleanup below, which aborts all listeners, removes the managed
|
|
22
|
+
* novalidate attribute, and releases the form's internal state.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import enhance from "./enhance.js";
|
|
26
|
+
import { EVENTS } from "./events.js";
|
|
27
|
+
import { CLASSES } from "./selectors.js";
|
|
28
|
+
|
|
29
|
+
const NOVALIDATE = "novalidate";
|
|
30
|
+
const WAS_VALIDATED_CLASS = CLASSES.wasValidated;
|
|
31
|
+
const NEEDS_VALIDATION_CLASS = CLASSES.needsValidation;
|
|
32
|
+
const FIELD_CLASS = CLASSES.field;
|
|
33
|
+
const DANGER_CLASS = CLASSES.danger;
|
|
34
|
+
const MANAGED_NOVALIDATE_ATTR = "validationManagedNovalidate";
|
|
35
|
+
const connectedForms = new WeakMap();
|
|
36
|
+
const warnedRules = new WeakMap();
|
|
37
|
+
|
|
38
|
+
function validDateParts(year, month, day) {
|
|
39
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
40
|
+
const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
41
|
+
return day <= days;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isValidDate(value) {
|
|
45
|
+
const v = value.trim();
|
|
46
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
47
|
+
if (iso) {
|
|
48
|
+
return validDateParts(Number(iso[1]), Number(iso[2]), Number(iso[3]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const local = /^(\d{1,2})([/.-])(\d{1,2})\2(\d{4})$/.exec(v);
|
|
52
|
+
if (!local) return false;
|
|
53
|
+
|
|
54
|
+
const first = Number(local[1]);
|
|
55
|
+
const second = Number(local[3]);
|
|
56
|
+
const year = Number(local[4]);
|
|
57
|
+
return validDateParts(year, second, first) || validDateParts(year, first, second);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function warnUnknownRuleOnce(el, name) {
|
|
61
|
+
let warned = warnedRules.get(el);
|
|
62
|
+
if (!warned) {
|
|
63
|
+
warned = new Set();
|
|
64
|
+
warnedRules.set(el, warned);
|
|
65
|
+
}
|
|
66
|
+
if (warned.has(name)) return;
|
|
67
|
+
warned.add(name);
|
|
68
|
+
console.warn(`Unknown validation rule "${name}" on ${el.name || el.id || "field"}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rules = {
|
|
72
|
+
same(v, el, selector) {
|
|
73
|
+
if (!selector) return false;
|
|
74
|
+
try {
|
|
75
|
+
const target = el.form?.querySelector(selector);
|
|
76
|
+
if (!target) return false;
|
|
77
|
+
return v === target.value;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
number(v) {
|
|
83
|
+
return !Number.isNaN(Number(v));
|
|
84
|
+
},
|
|
85
|
+
digits(v) {
|
|
86
|
+
return /^\d+$/.test(v);
|
|
87
|
+
},
|
|
88
|
+
alnum(v) {
|
|
89
|
+
return /^[a-z0-9]+$/i.test(v);
|
|
90
|
+
},
|
|
91
|
+
date(v) {
|
|
92
|
+
return isValidDate(v);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function isFormElement(el) {
|
|
97
|
+
return typeof HTMLFormElement !== "undefined" && el instanceof HTMLFormElement;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ignoreField(field) {
|
|
101
|
+
return (
|
|
102
|
+
!field ||
|
|
103
|
+
field.disabled ||
|
|
104
|
+
field.willValidate === false ||
|
|
105
|
+
typeof field.checkValidity !== "function" ||
|
|
106
|
+
typeof field.setCustomValidity !== "function"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function fieldContainer(el) {
|
|
111
|
+
return el.closest?.(`.${FIELD_CLASS}`) || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// .field.danger is validation-owned state, added/removed here based on
|
|
115
|
+
// aria-invalid/data-validation-errors on the field's controls. A .danger
|
|
116
|
+
// applied manually by app code for an unrelated reason will be removed the
|
|
117
|
+
// next time this runs.
|
|
118
|
+
function syncFieldDangerState(el) {
|
|
119
|
+
const field = fieldContainer(el);
|
|
120
|
+
if (!field) return;
|
|
121
|
+
|
|
122
|
+
if (field.querySelector('[aria-invalid="true"], [data-validation-errors]')) {
|
|
123
|
+
field.classList.add(DANGER_CLASS);
|
|
124
|
+
} else {
|
|
125
|
+
field.classList.remove(DANGER_CLASS);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkRules(el) {
|
|
130
|
+
if (el.dataset.validationErrors === "server") {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
el.setCustomValidity("");
|
|
134
|
+
delete el.dataset.validationErrors;
|
|
135
|
+
|
|
136
|
+
let valid = el.checkValidity();
|
|
137
|
+
|
|
138
|
+
const rulesAttr = el.dataset.validationRules;
|
|
139
|
+
if (rulesAttr) {
|
|
140
|
+
const hasValue = el.value.trim().length > 0;
|
|
141
|
+
const failed = [];
|
|
142
|
+
for (const entry of rulesAttr.split(",")) {
|
|
143
|
+
if (!hasValue) continue;
|
|
144
|
+
const trimmed = entry.trim();
|
|
145
|
+
if (!trimmed) continue;
|
|
146
|
+
const [name, ...opts] = trimmed.split(/\s+/);
|
|
147
|
+
const handler = rules[name];
|
|
148
|
+
if (!handler) {
|
|
149
|
+
warnUnknownRuleOnce(el, name);
|
|
150
|
+
failed.push(name);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let result = false;
|
|
155
|
+
try {
|
|
156
|
+
result = handler(el.value, el, ...opts);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`Validation rule "${name}" failed on ${el.name || el.id || "field"}`, error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!result) {
|
|
162
|
+
failed.push(name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (failed.length > 0) {
|
|
166
|
+
valid = false;
|
|
167
|
+
el.setCustomValidity(failed.join(", "));
|
|
168
|
+
el.dataset.validationErrors = failed.join(",");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return valid;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function errorEl(el) {
|
|
176
|
+
const ids = el.getAttribute("aria-describedby");
|
|
177
|
+
if (!ids) return null;
|
|
178
|
+
for (const id of ids.split(/\s+/)) {
|
|
179
|
+
const node = el.ownerDocument.getElementById(id);
|
|
180
|
+
if (node?.classList.contains(CLASSES.fieldError)) {
|
|
181
|
+
return node;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function markInvalid(el) {
|
|
188
|
+
el.setAttribute("aria-invalid", "true");
|
|
189
|
+
fieldContainer(el)?.classList.add(DANGER_CLASS);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function markValid(el) {
|
|
193
|
+
el.removeAttribute("aria-invalid");
|
|
194
|
+
delete el.dataset.validationErrors;
|
|
195
|
+
syncFieldDangerState(el);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function hydrateServerErrors(form) {
|
|
199
|
+
for (const el of form.elements) {
|
|
200
|
+
if (ignoreField(el)) continue;
|
|
201
|
+
if (el.getAttribute("aria-invalid") === "true" && !el.dataset.validationErrors) {
|
|
202
|
+
el.dataset.validationErrors = "server";
|
|
203
|
+
}
|
|
204
|
+
syncFieldDangerState(el);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ensureManagedNoValidate(form) {
|
|
209
|
+
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) return;
|
|
210
|
+
if (form.hasAttribute(NOVALIDATE)) return;
|
|
211
|
+
|
|
212
|
+
form.setAttribute(NOVALIDATE, "");
|
|
213
|
+
form.dataset[MANAGED_NOVALIDATE_ATTR] = "";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function releaseManagedNoValidate(form) {
|
|
217
|
+
if (form.dataset[MANAGED_NOVALIDATE_ATTR] === undefined) return;
|
|
218
|
+
form.removeAttribute(NOVALIDATE);
|
|
219
|
+
delete form.dataset[MANAGED_NOVALIDATE_ATTR];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateField(el, trigger) {
|
|
223
|
+
const form = el.form;
|
|
224
|
+
if (!form?.classList.contains(NEEDS_VALIDATION_CLASS)) return;
|
|
225
|
+
if (ignoreField(el)) return;
|
|
226
|
+
|
|
227
|
+
if (trigger === "input" && el.dataset.validationErrors === "server") {
|
|
228
|
+
delete el.dataset.validationErrors;
|
|
229
|
+
el.setCustomValidity("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const validationTrigger = el.dataset.validationTrigger || "";
|
|
233
|
+
const alreadyInvalid = el.getAttribute("aria-invalid") === "true" || el.dataset.validationErrors;
|
|
234
|
+
const wantsTrigger =
|
|
235
|
+
trigger === "blur" || (validationTrigger.includes(trigger) && el.value.length > 0);
|
|
236
|
+
|
|
237
|
+
if (!wantsTrigger && !alreadyInvalid) return;
|
|
238
|
+
|
|
239
|
+
if (checkRules(el)) {
|
|
240
|
+
markValid(el);
|
|
241
|
+
} else {
|
|
242
|
+
markInvalid(el);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function connectForm(form) {
|
|
247
|
+
const existing = connectedForms.get(form);
|
|
248
|
+
if (existing) return existing;
|
|
249
|
+
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
|
|
252
|
+
if (!form.hasAttribute(NOVALIDATE)) {
|
|
253
|
+
form.setAttribute(NOVALIDATE, "");
|
|
254
|
+
form.dataset[MANAGED_NOVALIDATE_ATTR] = "";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
hydrateServerErrors(form);
|
|
258
|
+
|
|
259
|
+
form.addEventListener(
|
|
260
|
+
"focusout",
|
|
261
|
+
(event) => {
|
|
262
|
+
const el = event.target;
|
|
263
|
+
if (el instanceof Element && el.form === form) {
|
|
264
|
+
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
265
|
+
releaseManagedNoValidate(form);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
ensureManagedNoValidate(form);
|
|
269
|
+
validateField(el, "blur");
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{ signal: controller.signal },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
form.addEventListener(
|
|
276
|
+
"input",
|
|
277
|
+
(event) => {
|
|
278
|
+
const el = event.target;
|
|
279
|
+
if (el instanceof Element && el.form === form) {
|
|
280
|
+
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
281
|
+
releaseManagedNoValidate(form);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
ensureManagedNoValidate(form);
|
|
285
|
+
validateField(el, "input");
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{ signal: controller.signal },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
form.addEventListener(
|
|
292
|
+
"reset",
|
|
293
|
+
() => {
|
|
294
|
+
form.classList.remove(WAS_VALIDATED_CLASS);
|
|
295
|
+
for (const el of form.elements) {
|
|
296
|
+
if (ignoreField(el)) continue;
|
|
297
|
+
el.setCustomValidity("");
|
|
298
|
+
markValid(el);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{ signal: controller.signal },
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
form.addEventListener(
|
|
305
|
+
"submit",
|
|
306
|
+
(event) => {
|
|
307
|
+
if (event.submitter?.formNoValidate) return;
|
|
308
|
+
|
|
309
|
+
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
310
|
+
releaseManagedNoValidate(form);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
ensureManagedNoValidate(form);
|
|
315
|
+
|
|
316
|
+
let firstInvalid = null;
|
|
317
|
+
|
|
318
|
+
for (const el of form.elements) {
|
|
319
|
+
if (ignoreField(el)) continue;
|
|
320
|
+
|
|
321
|
+
if (checkRules(el)) {
|
|
322
|
+
markValid(el);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
markInvalid(el);
|
|
327
|
+
if (!firstInvalid) firstInvalid = el;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
form.classList.add(WAS_VALIDATED_CLASS);
|
|
331
|
+
|
|
332
|
+
if (!firstInvalid) return;
|
|
333
|
+
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
event.stopPropagation();
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
firstInvalid.focus();
|
|
339
|
+
} catch {
|
|
340
|
+
// Some environments do not implement focus on detached elements.
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
form.dispatchEvent(
|
|
344
|
+
new CustomEvent(EVENTS.invalid, {
|
|
345
|
+
bubbles: true,
|
|
346
|
+
detail: {
|
|
347
|
+
form,
|
|
348
|
+
firstInvalid,
|
|
349
|
+
message: form.dataset.validationMessage ?? "",
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
{ signal: controller.signal },
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const cleanup = () => {
|
|
358
|
+
controller.abort();
|
|
359
|
+
releaseManagedNoValidate(form);
|
|
360
|
+
connectedForms.delete(form);
|
|
361
|
+
};
|
|
362
|
+
connectedForms.set(form, cleanup);
|
|
363
|
+
return cleanup;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: Stable public namespace for validation helpers and rules.
|
|
367
|
+
export class FormValidator {
|
|
368
|
+
static get rules() {
|
|
369
|
+
return rules;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static registerRule(name, callback) {
|
|
373
|
+
rules[name] = callback;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
static init(selector = `.${NEEDS_VALIDATION_CLASS}`) {
|
|
377
|
+
if (typeof document === "undefined") return;
|
|
378
|
+
for (const form of document.querySelectorAll(selector)) {
|
|
379
|
+
if (isFormElement(form)) connectForm(form);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
static setFieldError(el, message) {
|
|
384
|
+
el.setCustomValidity(message || "invalid");
|
|
385
|
+
el.dataset.validationErrors = "server";
|
|
386
|
+
markInvalid(el);
|
|
387
|
+
const error = errorEl(el);
|
|
388
|
+
if (error && message) error.textContent = message;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static clearFieldError(el) {
|
|
392
|
+
delete el.dataset.validationErrors;
|
|
393
|
+
markValid(el);
|
|
394
|
+
el.setCustomValidity("");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
static setErrors(form, errors) {
|
|
398
|
+
if (!isFormElement(form)) return;
|
|
399
|
+
for (const [name, message] of Object.entries(errors)) {
|
|
400
|
+
const found = form.elements[name];
|
|
401
|
+
if (!found) continue;
|
|
402
|
+
if (found instanceof Element) {
|
|
403
|
+
FormValidator.setFieldError(found, message);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const el of found) {
|
|
407
|
+
FormValidator.setFieldError(el, message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (typeof document !== "undefined") {
|
|
414
|
+
enhance({
|
|
415
|
+
[`form.${NEEDS_VALIDATION_CLASS}`]: (form) => connectForm(form),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default FormValidator;
|