vanilla-jui 1.4.7 → 1.5.1
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 +10 -4
- package/dist/index.d.ts +418 -970
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +466 -346
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +198 -328
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +273 -367
- package/src/components/pagination.ts +201 -375
- package/src/components/swiper.ts +689 -1107
- package/src/components/tabs.ts +467 -778
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +5 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -2
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
3
|
import { type DOMReference, all, resolveElement } from '../utilities/dom.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { createEventManager } from '../utilities/events.ts';
|
|
5
5
|
import {
|
|
6
6
|
type ResolveSchema,
|
|
7
7
|
resolveProps,
|
|
@@ -53,26 +53,18 @@ interface ResolvedValidatorProps extends Record<string, unknown> {
|
|
|
53
53
|
onSubmit: ((validator: ValidatorInstance) => void) | null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
interface ValidatorDOM {
|
|
57
|
-
root: HTMLFormElement | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
56
|
interface ValidatorRuntime {
|
|
61
57
|
valid: boolean;
|
|
62
58
|
message: string;
|
|
63
59
|
destroyed: boolean;
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
interface ValidatorCleanup {
|
|
67
|
-
events: IEventManager;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
62
|
interface ResetOptions {
|
|
71
63
|
native?: boolean;
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
interface ValidatorInstance {
|
|
75
|
-
|
|
66
|
+
export interface ValidatorInstance {
|
|
67
|
+
readonly element: HTMLFormElement | null;
|
|
76
68
|
props: ResolvedValidatorProps | null;
|
|
77
69
|
runtime: ValidatorRuntime;
|
|
78
70
|
validate(): boolean;
|
|
@@ -146,462 +138,370 @@ function hasFiles(element: HTMLInputElement): element is HTMLInputElement & {
|
|
|
146
138
|
return !!element.files && element.files.length > 0;
|
|
147
139
|
}
|
|
148
140
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
function validateRequired(
|
|
142
|
+
element: ValidatorElement,
|
|
143
|
+
required: unknown
|
|
144
|
+
): boolean {
|
|
145
|
+
if (required !== true) return true;
|
|
146
|
+
if (element instanceof HTMLSelectElement) {
|
|
147
|
+
return selectedValues(element).some((value) => value.length > 0);
|
|
148
|
+
}
|
|
149
|
+
return element.value.trim().length >= 1;
|
|
150
|
+
}
|
|
159
151
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
152
|
+
function validateMinLength(
|
|
153
|
+
element: ValidatorElement,
|
|
154
|
+
minLength: unknown
|
|
155
|
+
): boolean {
|
|
156
|
+
if (typeof minLength !== 'number') return true;
|
|
157
|
+
return element.value.length >= minLength;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateMaxLength(
|
|
161
|
+
element: ValidatorElement,
|
|
162
|
+
maxLength: unknown
|
|
163
|
+
): boolean {
|
|
164
|
+
if (typeof maxLength !== 'number') return true;
|
|
165
|
+
return element.value.length <= maxLength;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateEmail(element: ValidatorElement): boolean {
|
|
169
|
+
const emailPattern = /^([\w-.]+@([\w-]+\.)+[\w-]{2,4})?$/;
|
|
170
|
+
return emailPattern.test(element.value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateEqualTo(
|
|
174
|
+
root: HTMLFormElement | null,
|
|
175
|
+
element: ValidatorElement,
|
|
176
|
+
targetName: unknown
|
|
177
|
+
): boolean {
|
|
178
|
+
if (!root || typeof targetName !== 'string') return true;
|
|
179
|
+
const targetElement = toValidatorElement(root.elements.namedItem(targetName));
|
|
180
|
+
if (!targetElement) {
|
|
181
|
+
throw new Error(`Validator: target element "${targetName}" not found.`);
|
|
179
182
|
}
|
|
183
|
+
return element.value === targetElement.value;
|
|
184
|
+
}
|
|
180
185
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return root;
|
|
186
|
+
function validateCheck(element: ValidatorElement, checked: unknown): boolean {
|
|
187
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Validator: element expects a checkbox input, but ${element.tagName.toLowerCase()} given.`
|
|
190
|
+
);
|
|
187
191
|
}
|
|
192
|
+
return element.checked === checked;
|
|
193
|
+
}
|
|
188
194
|
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
function validateSelected(
|
|
196
|
+
element: ValidatorElement,
|
|
197
|
+
selected: unknown
|
|
198
|
+
): boolean {
|
|
199
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
|
|
202
|
+
);
|
|
191
203
|
}
|
|
204
|
+
if (selected !== true) return true;
|
|
205
|
+
return selectedValues(element).some((value) => value.length > 0);
|
|
206
|
+
}
|
|
192
207
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
function validateMultiple(
|
|
209
|
+
element: ValidatorElement,
|
|
210
|
+
multiple: unknown
|
|
211
|
+
): boolean {
|
|
212
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (multiple !== true) return true;
|
|
218
|
+
return element.selectedOptions.length > 0;
|
|
219
|
+
}
|
|
197
220
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
202
|
-
this.cleanup.events.on('reset', root, 'reset', () => {
|
|
203
|
-
this.reset({ native: false });
|
|
204
|
-
});
|
|
221
|
+
function validateSelectMin(element: ValidatorElement, min: unknown): boolean {
|
|
222
|
+
if (!(element instanceof HTMLSelectElement) || typeof min !== 'number') {
|
|
223
|
+
return true;
|
|
205
224
|
}
|
|
225
|
+
return element.selectedOptions.length >= min;
|
|
226
|
+
}
|
|
206
227
|
|
|
207
|
-
|
|
208
|
-
|
|
228
|
+
function validateSelectMax(element: ValidatorElement, max: unknown): boolean {
|
|
229
|
+
if (!(element instanceof HTMLSelectElement) || typeof max !== 'number') {
|
|
230
|
+
return true;
|
|
209
231
|
}
|
|
232
|
+
return element.selectedOptions.length <= max;
|
|
233
|
+
}
|
|
210
234
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
validate(): boolean {
|
|
215
|
-
const { root } = this.dom;
|
|
216
|
-
if (!root || !this.props) return false;
|
|
235
|
+
function validateNoSpace(element: ValidatorElement, noSpace: unknown): boolean {
|
|
236
|
+
return !/\s/.test(element.value) || noSpace !== true;
|
|
237
|
+
}
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
function validatePattern(element: ValidatorElement, pattern: unknown): boolean {
|
|
240
|
+
if (!(typeof pattern === 'string' || pattern instanceof RegExp)) return true;
|
|
241
|
+
return new RegExp(pattern).test(element.value);
|
|
242
|
+
}
|
|
220
243
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
function validateFile(element: ValidatorElement, required: unknown): boolean {
|
|
245
|
+
if (!isFileInput(element)) return true;
|
|
246
|
+
return required === true ? hasFiles(element) : true;
|
|
247
|
+
}
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
function validateMinSize(element: ValidatorElement, minSize: unknown): boolean {
|
|
250
|
+
if (
|
|
251
|
+
!isFileInput(element) ||
|
|
252
|
+
!hasFiles(element) ||
|
|
253
|
+
typeof minSize !== 'number'
|
|
254
|
+
) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return element.files[0].size >= minSize;
|
|
258
|
+
}
|
|
228
259
|
|
|
229
|
-
|
|
230
|
-
|
|
260
|
+
function validateMaxSize(element: ValidatorElement, maxSize: unknown): boolean {
|
|
261
|
+
if (
|
|
262
|
+
!isFileInput(element) ||
|
|
263
|
+
!hasFiles(element) ||
|
|
264
|
+
typeof maxSize !== 'number'
|
|
265
|
+
) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return element.files[0].size <= maxSize;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validateAccept(element: ValidatorElement, accept: unknown): boolean {
|
|
272
|
+
if (
|
|
273
|
+
!isFileInput(element) ||
|
|
274
|
+
!hasFiles(element) ||
|
|
275
|
+
typeof accept !== 'string'
|
|
276
|
+
) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const file = element.files[0];
|
|
281
|
+
const allowed = accept.split(',').map((item) => item.trim().toLowerCase());
|
|
282
|
+
|
|
283
|
+
return allowed.some((rule) => {
|
|
284
|
+
if (rule.startsWith('.')) {
|
|
285
|
+
return file.name.toLowerCase().endsWith(rule);
|
|
286
|
+
}
|
|
287
|
+
if (rule.endsWith('/*')) {
|
|
288
|
+
return file.type.startsWith(rule.replace('/*', '/'));
|
|
231
289
|
}
|
|
232
|
-
return
|
|
290
|
+
return file.type === rule;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function showError(
|
|
295
|
+
props: ResolvedValidatorProps | null,
|
|
296
|
+
runtime: ValidatorRuntime,
|
|
297
|
+
element: ValidatorElement,
|
|
298
|
+
name: string,
|
|
299
|
+
rule: string,
|
|
300
|
+
customMessage = ''
|
|
301
|
+
): void {
|
|
302
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
|
|
303
|
+
element.classList.remove('is-valid');
|
|
304
|
+
element.classList.add('is-invalid');
|
|
233
305
|
}
|
|
234
306
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
307
|
+
const error = customMessage || props?.messages[name]?.[rule] || '';
|
|
308
|
+
if (!error) return;
|
|
309
|
+
|
|
310
|
+
runtime.message = error;
|
|
239
311
|
|
|
240
|
-
|
|
241
|
-
|
|
312
|
+
const formControl = getControlContainer(element);
|
|
313
|
+
if (!formControl) return;
|
|
314
|
+
|
|
315
|
+
let help =
|
|
316
|
+
all<HTMLElement>('[data-validator-help]', formControl).find(
|
|
317
|
+
(element) => element.dataset.validatorHelp === name
|
|
318
|
+
) || null;
|
|
319
|
+
if (!help) {
|
|
320
|
+
help = jsx('div', {
|
|
321
|
+
className: 'help-block is-invalid',
|
|
322
|
+
'data-validator-help': name,
|
|
323
|
+
}) as HTMLElement;
|
|
324
|
+
formControl.appendChild(help);
|
|
325
|
+
}
|
|
326
|
+
help.textContent = error;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function showSuccess(element: ValidatorElement): void {
|
|
330
|
+
const formControl = getControlContainer(element);
|
|
331
|
+
if (formControl) {
|
|
332
|
+
for (const help of all<HTMLElement>('[data-validator-help]', formControl)) {
|
|
333
|
+
if (help.dataset.validatorHelp === element.name) help.remove();
|
|
242
334
|
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
|
|
338
|
+
element.classList.remove('is-invalid');
|
|
339
|
+
element.classList.add('is-valid');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function createValidator(
|
|
344
|
+
element: DOMReference,
|
|
345
|
+
props: ValidatorProps = {},
|
|
346
|
+
bindEvents = false
|
|
347
|
+
): ValidatorInstance {
|
|
348
|
+
validateParam('bindEvents', bindEvents, 'boolean', 'Validator');
|
|
349
|
+
const resolved = resolveElement(element);
|
|
350
|
+
if (!(resolved instanceof HTMLFormElement)) {
|
|
351
|
+
throw new Error('Validator.element expects a form element.');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let root: HTMLFormElement | null = resolved;
|
|
355
|
+
let options: ResolvedValidatorProps | null = normalizeProps(props);
|
|
356
|
+
const runtime = createRuntime();
|
|
357
|
+
const events = createEventManager();
|
|
358
|
+
let validator: ValidatorInstance;
|
|
359
|
+
|
|
360
|
+
const validateCustom = (
|
|
361
|
+
control: ValidatorElement,
|
|
362
|
+
rule: unknown
|
|
363
|
+
): ValidatorCustomResult => {
|
|
364
|
+
if (typeof rule !== 'function') return true;
|
|
365
|
+
const result = (rule as ValidatorCustomRule)(control, validator);
|
|
366
|
+
return typeof result === 'string' ? result : !!result;
|
|
367
|
+
};
|
|
368
|
+
const validateRule = (control: ValidatorElement, name: string): boolean => {
|
|
369
|
+
const rules = options?.rules[name];
|
|
370
|
+
if (!rules) return true;
|
|
243
371
|
|
|
244
372
|
for (const rule of Object.keys(rules)) {
|
|
245
373
|
const value = rules[rule];
|
|
246
374
|
let customMessage = '';
|
|
247
|
-
|
|
248
375
|
switch (rule) {
|
|
249
376
|
case 'required':
|
|
250
|
-
|
|
377
|
+
runtime.valid = validateRequired(control, value);
|
|
251
378
|
break;
|
|
252
379
|
case 'minLength':
|
|
253
|
-
|
|
380
|
+
runtime.valid = validateMinLength(control, value);
|
|
254
381
|
break;
|
|
255
382
|
case 'maxLength':
|
|
256
|
-
|
|
383
|
+
runtime.valid = validateMaxLength(control, value);
|
|
257
384
|
break;
|
|
258
385
|
case 'equalTo':
|
|
259
|
-
|
|
386
|
+
runtime.valid = validateEqualTo(root, control, value);
|
|
260
387
|
break;
|
|
261
388
|
case 'email':
|
|
262
|
-
|
|
389
|
+
runtime.valid = validateEmail(control);
|
|
263
390
|
break;
|
|
264
391
|
case 'checked':
|
|
265
|
-
|
|
392
|
+
runtime.valid = validateCheck(control, value);
|
|
266
393
|
break;
|
|
267
394
|
case 'selected':
|
|
268
|
-
|
|
395
|
+
runtime.valid = validateSelected(control, value);
|
|
269
396
|
break;
|
|
270
397
|
case 'multiple':
|
|
271
|
-
|
|
398
|
+
runtime.valid = validateMultiple(control, value);
|
|
272
399
|
break;
|
|
273
400
|
case 'min':
|
|
274
|
-
|
|
401
|
+
runtime.valid = validateSelectMin(control, value);
|
|
275
402
|
break;
|
|
276
403
|
case 'max':
|
|
277
|
-
|
|
404
|
+
runtime.valid = validateSelectMax(control, value);
|
|
278
405
|
break;
|
|
279
406
|
case 'noSpace':
|
|
280
|
-
|
|
407
|
+
runtime.valid = validateNoSpace(control, value);
|
|
281
408
|
break;
|
|
282
409
|
case 'noChinese':
|
|
283
|
-
|
|
410
|
+
runtime.valid = !/[\u4e00-\u9fa5]/.test(control.value);
|
|
284
411
|
break;
|
|
285
412
|
case 'noSpecial':
|
|
286
|
-
|
|
413
|
+
runtime.valid = !/[@#$%^&*]+/g.test(control.value);
|
|
287
414
|
break;
|
|
288
415
|
case 'pattern':
|
|
289
|
-
|
|
416
|
+
runtime.valid = validatePattern(control, value);
|
|
290
417
|
break;
|
|
291
418
|
case 'file':
|
|
292
|
-
|
|
419
|
+
runtime.valid = validateFile(control, value);
|
|
293
420
|
break;
|
|
294
421
|
case 'minSize':
|
|
295
|
-
|
|
422
|
+
runtime.valid = validateMinSize(control, value);
|
|
296
423
|
break;
|
|
297
424
|
case 'maxSize':
|
|
298
|
-
|
|
425
|
+
runtime.valid = validateMaxSize(control, value);
|
|
299
426
|
break;
|
|
300
427
|
case 'accept':
|
|
301
|
-
|
|
428
|
+
runtime.valid = validateAccept(control, value);
|
|
302
429
|
break;
|
|
303
430
|
case 'validate': {
|
|
304
|
-
const result =
|
|
305
|
-
|
|
431
|
+
const result = validateCustom(control, value);
|
|
432
|
+
runtime.valid = result === true;
|
|
306
433
|
customMessage = typeof result === 'string' ? result : '';
|
|
307
434
|
break;
|
|
308
435
|
}
|
|
309
436
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.showError(element, name, rule, customMessage);
|
|
437
|
+
if (!runtime.valid) {
|
|
438
|
+
showError(options, runtime, control, name, rule, customMessage);
|
|
313
439
|
break;
|
|
314
440
|
}
|
|
315
|
-
|
|
316
|
-
this.showSuccess(element);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return this.runtime.valid;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private hasNativeValidationRule(element: ValidatorElement): boolean {
|
|
323
|
-
return (
|
|
324
|
-
element.hasAttribute('required') ||
|
|
325
|
-
element.hasAttribute('minlength') ||
|
|
326
|
-
element.hasAttribute('maxlength') ||
|
|
327
|
-
element.hasAttribute('pattern') ||
|
|
328
|
-
element.hasAttribute('min') ||
|
|
329
|
-
element.hasAttribute('max') ||
|
|
330
|
-
element.hasAttribute('step')
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private validateRequired(
|
|
335
|
-
element: ValidatorElement,
|
|
336
|
-
required: unknown
|
|
337
|
-
): boolean {
|
|
338
|
-
if (required !== true) return true;
|
|
339
|
-
if (element instanceof HTMLSelectElement) {
|
|
340
|
-
return selectedValues(element).some((value) => value.length > 0);
|
|
341
|
-
}
|
|
342
|
-
return element.value.trim().length >= 1;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private validateMinLength(
|
|
346
|
-
element: ValidatorElement,
|
|
347
|
-
minLength: unknown
|
|
348
|
-
): boolean {
|
|
349
|
-
if (typeof minLength !== 'number') return true;
|
|
350
|
-
return element.value.length >= minLength;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private validateMaxLength(
|
|
354
|
-
element: ValidatorElement,
|
|
355
|
-
maxLength: unknown
|
|
356
|
-
): boolean {
|
|
357
|
-
if (typeof maxLength !== 'number') return true;
|
|
358
|
-
return element.value.length <= maxLength;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private validateEmail(element: ValidatorElement): boolean {
|
|
362
|
-
const emailPattern = /^([\w-.]+@([\w-]+\.)+[\w-]{2,4})?$/;
|
|
363
|
-
return emailPattern.test(element.value);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private validateEqualTo(
|
|
367
|
-
element: ValidatorElement,
|
|
368
|
-
targetName: unknown
|
|
369
|
-
): boolean {
|
|
370
|
-
const { root } = this.dom;
|
|
371
|
-
if (!root || typeof targetName !== 'string') return true;
|
|
372
|
-
const targetElement = toValidatorElement(
|
|
373
|
-
root.elements.namedItem(targetName)
|
|
374
|
-
);
|
|
375
|
-
if (!targetElement) {
|
|
376
|
-
throw new Error(`Validator: target element "${targetName}" not found.`);
|
|
377
|
-
}
|
|
378
|
-
return element.value === targetElement.value;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private validateCheck(element: ValidatorElement, checked: unknown): boolean {
|
|
382
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
|
|
383
|
-
throw new Error(
|
|
384
|
-
`Validator: element expects a checkbox input, but ${element.tagName.toLowerCase()} given.`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
return element.checked === checked;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private validateSelected(
|
|
391
|
-
element: ValidatorElement,
|
|
392
|
-
selected: unknown
|
|
393
|
-
): boolean {
|
|
394
|
-
if (!(element instanceof HTMLSelectElement)) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
if (selected !== true) return true;
|
|
400
|
-
return selectedValues(element).some((value) => value.length > 0);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private validateMultiple(
|
|
404
|
-
element: ValidatorElement,
|
|
405
|
-
multiple: unknown
|
|
406
|
-
): boolean {
|
|
407
|
-
if (!(element instanceof HTMLSelectElement)) {
|
|
408
|
-
throw new Error(
|
|
409
|
-
`Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
if (multiple !== true) return true;
|
|
413
|
-
return element.selectedOptions.length > 0;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private validateSelectMin(element: ValidatorElement, min: unknown): boolean {
|
|
417
|
-
if (!(element instanceof HTMLSelectElement) || typeof min !== 'number') {
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
return element.selectedOptions.length >= min;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private validateSelectMax(element: ValidatorElement, max: unknown): boolean {
|
|
424
|
-
if (!(element instanceof HTMLSelectElement) || typeof max !== 'number') {
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
return element.selectedOptions.length <= max;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private validateNoSpace(
|
|
431
|
-
element: ValidatorElement,
|
|
432
|
-
noSpace: unknown
|
|
433
|
-
): boolean {
|
|
434
|
-
return !/\s/.test(element.value) || noSpace !== true;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private validatePattern(
|
|
438
|
-
element: ValidatorElement,
|
|
439
|
-
pattern: unknown
|
|
440
|
-
): boolean {
|
|
441
|
-
if (!(typeof pattern === 'string' || pattern instanceof RegExp))
|
|
442
|
-
return true;
|
|
443
|
-
return new RegExp(pattern).test(element.value);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private validateFile(element: ValidatorElement, required: unknown): boolean {
|
|
447
|
-
if (!isFileInput(element)) return true;
|
|
448
|
-
return required === true ? hasFiles(element) : true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
private validateMinSize(
|
|
452
|
-
element: ValidatorElement,
|
|
453
|
-
minSize: unknown
|
|
454
|
-
): boolean {
|
|
455
|
-
if (
|
|
456
|
-
!isFileInput(element) ||
|
|
457
|
-
!hasFiles(element) ||
|
|
458
|
-
typeof minSize !== 'number'
|
|
459
|
-
) {
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
return element.files[0].size >= minSize;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private validateMaxSize(
|
|
466
|
-
element: ValidatorElement,
|
|
467
|
-
maxSize: unknown
|
|
468
|
-
): boolean {
|
|
469
|
-
if (
|
|
470
|
-
!isFileInput(element) ||
|
|
471
|
-
!hasFiles(element) ||
|
|
472
|
-
typeof maxSize !== 'number'
|
|
473
|
-
) {
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
return element.files[0].size <= maxSize;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private validateAccept(element: ValidatorElement, accept: unknown): boolean {
|
|
480
|
-
if (
|
|
481
|
-
!isFileInput(element) ||
|
|
482
|
-
!hasFiles(element) ||
|
|
483
|
-
typeof accept !== 'string'
|
|
484
|
-
) {
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const file = element.files[0];
|
|
489
|
-
const allowed = accept.split(',').map((item) => item.trim().toLowerCase());
|
|
490
|
-
|
|
491
|
-
return allowed.some((rule) => {
|
|
492
|
-
if (rule.startsWith('.')) {
|
|
493
|
-
return file.name.toLowerCase().endsWith(rule);
|
|
494
|
-
}
|
|
495
|
-
if (rule.endsWith('/*')) {
|
|
496
|
-
return file.type.startsWith(rule.replace('/*', '/'));
|
|
497
|
-
}
|
|
498
|
-
return file.type === rule;
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
private validateCustom(
|
|
503
|
-
element: ValidatorElement,
|
|
504
|
-
rule: unknown
|
|
505
|
-
): ValidatorCustomResult {
|
|
506
|
-
if (typeof rule !== 'function') return true;
|
|
507
|
-
const result = (rule as ValidatorCustomRule)(element, this);
|
|
508
|
-
return typeof result === 'string' ? result : !!result;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private showError(
|
|
512
|
-
element: ValidatorElement,
|
|
513
|
-
name: string,
|
|
514
|
-
rule: string,
|
|
515
|
-
customMessage = ''
|
|
516
|
-
): void {
|
|
517
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
|
|
518
|
-
element.classList.remove('is-valid');
|
|
519
|
-
element.classList.add('is-invalid');
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const error = customMessage || this.props?.messages[name]?.[rule] || '';
|
|
523
|
-
if (!error) return;
|
|
524
|
-
|
|
525
|
-
this.runtime.message = error;
|
|
526
|
-
|
|
527
|
-
const formControl = getControlContainer(element);
|
|
528
|
-
if (!formControl) return;
|
|
529
|
-
|
|
530
|
-
let help =
|
|
531
|
-
all<HTMLElement>('[data-validator-help]', formControl).find(
|
|
532
|
-
(element) => element.dataset.validatorHelp === name
|
|
533
|
-
) || null;
|
|
534
|
-
if (!help) {
|
|
535
|
-
help = jsx('div', {
|
|
536
|
-
className: 'help-block is-invalid',
|
|
537
|
-
'data-validator-help': name,
|
|
538
|
-
}) as HTMLElement;
|
|
539
|
-
formControl.appendChild(help);
|
|
540
|
-
}
|
|
541
|
-
help.textContent = error;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
private showSuccess(element: ValidatorElement): void {
|
|
545
|
-
const formControl = getControlContainer(element);
|
|
546
|
-
if (formControl) {
|
|
547
|
-
for (const help of all<HTMLElement>(
|
|
548
|
-
'[data-validator-help]',
|
|
549
|
-
formControl
|
|
550
|
-
)) {
|
|
551
|
-
if (help.dataset.validatorHelp === element.name) help.remove();
|
|
552
|
-
}
|
|
441
|
+
showSuccess(control);
|
|
553
442
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
443
|
+
return runtime.valid;
|
|
444
|
+
};
|
|
445
|
+
const validate = (): boolean => {
|
|
446
|
+
if (!root || !options || runtime.destroyed) return false;
|
|
447
|
+
runtime.valid = true;
|
|
448
|
+
runtime.message = '';
|
|
449
|
+
for (const control of Array.from(root.elements)) {
|
|
450
|
+
if (!isValidatorElement(control) || !control.name) continue;
|
|
451
|
+
if (!options.rules[control.name]) continue;
|
|
452
|
+
runtime.valid = validateRule(control, control.name);
|
|
453
|
+
if (!runtime.valid) break;
|
|
558
454
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
*/
|
|
564
|
-
reset({ native = true }: ResetOptions = {}): void {
|
|
565
|
-
const { root } = this.dom;
|
|
455
|
+
if (runtime.valid) options.onSubmit?.(validator);
|
|
456
|
+
return runtime.valid;
|
|
457
|
+
};
|
|
458
|
+
const reset = ({ native = true }: ResetOptions = {}): void => {
|
|
566
459
|
if (!root) return;
|
|
567
460
|
if (native) root.reset();
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
element.classList.remove('is-invalid');
|
|
461
|
+
for (const control of Array.from(root.elements)) {
|
|
462
|
+
if (isValidatorElement(control)) {
|
|
463
|
+
control.classList.remove('is-valid', 'is-invalid');
|
|
464
|
+
}
|
|
573
465
|
}
|
|
574
|
-
|
|
575
466
|
for (const help of all<HTMLElement>('[data-validator-help]', root)) {
|
|
576
467
|
help.remove();
|
|
577
468
|
}
|
|
469
|
+
runtime.valid = true;
|
|
470
|
+
runtime.message = '';
|
|
471
|
+
};
|
|
472
|
+
const destroy = (): void => {
|
|
473
|
+
if (runtime.destroyed) return;
|
|
474
|
+
runtime.destroyed = true;
|
|
475
|
+
events.clear();
|
|
476
|
+
reset({ native: false });
|
|
477
|
+
root = null;
|
|
478
|
+
options = null;
|
|
479
|
+
runtime.valid = false;
|
|
480
|
+
runtime.message = '';
|
|
481
|
+
};
|
|
578
482
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
483
|
+
validator = {
|
|
484
|
+
get element() {
|
|
485
|
+
return root;
|
|
486
|
+
},
|
|
487
|
+
get props() {
|
|
488
|
+
return options;
|
|
489
|
+
},
|
|
490
|
+
set props(value) {
|
|
491
|
+
options = value;
|
|
492
|
+
},
|
|
493
|
+
runtime,
|
|
494
|
+
validate,
|
|
495
|
+
reset,
|
|
496
|
+
destroy,
|
|
497
|
+
};
|
|
582
498
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
this.unbindEvents();
|
|
591
|
-
this.reset({ native: false });
|
|
592
|
-
this.dom.root = null;
|
|
593
|
-
this.props = null;
|
|
594
|
-
this.runtime.valid = false;
|
|
595
|
-
this.runtime.message = '';
|
|
596
|
-
this.cleanup?.events.clear();
|
|
597
|
-
this.cleanup = null;
|
|
499
|
+
if (bindEvents) {
|
|
500
|
+
events.on('submit', root, 'submit', (event) => {
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
validate();
|
|
503
|
+
});
|
|
504
|
+
events.on('reset', root, 'reset', () => reset({ native: false }));
|
|
598
505
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
export function createValidator(
|
|
602
|
-
element: DOMReference,
|
|
603
|
-
props: ValidatorProps = {},
|
|
604
|
-
bindEvents = false
|
|
605
|
-
): ValidatorInstance {
|
|
606
|
-
return new Validator(element, props, bindEvents);
|
|
506
|
+
return validator;
|
|
607
507
|
}
|