vanilla-jui 1.5.9 → 1.5.11

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.
@@ -44,17 +44,20 @@ interface ValidatorRule extends Record<string, unknown> {
44
44
  interface ValidatorProps extends Record<string, unknown> {
45
45
  rules?: Record<string, ValidatorRule>;
46
46
  messages?: ValidatorMessageMap;
47
+ vanilla?: boolean;
47
48
  onSubmit?: ((validator: ValidatorInstance) => void) | null;
48
49
  }
49
50
 
50
51
  interface ResolvedValidatorProps extends Record<string, unknown> {
51
52
  rules: Record<string, ValidatorRule>;
52
53
  messages: ValidatorMessageMap;
54
+ vanilla: boolean;
53
55
  onSubmit: ((validator: ValidatorInstance) => void) | null;
54
56
  }
55
57
 
56
58
  interface ValidatorRuntime {
57
59
  valid: boolean;
60
+ error: boolean;
58
61
  message: string;
59
62
  destroyed: boolean;
60
63
  }
@@ -75,6 +78,7 @@ export interface ValidatorInstance {
75
78
  const VALIDATOR_PROPS_SCHEMA = {
76
79
  rules: { default: {}, type: 'object' },
77
80
  messages: { default: {}, type: 'object' },
81
+ vanilla: { default: false, type: 'boolean' },
78
82
  onSubmit: { default: null, types: ['function', 'null'] },
79
83
  } satisfies ResolveSchema<ValidatorProps>;
80
84
 
@@ -83,6 +87,7 @@ function normalizeProps(input: ValidatorProps): ResolvedValidatorProps {
83
87
  return {
84
88
  rules: props.rules as Record<string, ValidatorRule>,
85
89
  messages: props.messages as ValidatorMessageMap,
90
+ vanilla: props.vanilla as boolean,
86
91
  onSubmit: props.onSubmit as ResolvedValidatorProps['onSubmit'],
87
92
  };
88
93
  }
@@ -111,6 +116,7 @@ function toValidatorElement(
111
116
  function createRuntime(): ValidatorRuntime {
112
117
  return {
113
118
  valid: true,
119
+ error: false,
114
120
  message: '',
115
121
  destroyed: false,
116
122
  };
@@ -118,7 +124,8 @@ function createRuntime(): ValidatorRuntime {
118
124
 
119
125
  function getControlContainer(element: ValidatorElement): HTMLElement | null {
120
126
  return (
121
- element.closest<HTMLElement>('[data-form-control]') || element.parentElement
127
+ element.closest<HTMLElement>('[data-field-control]') ||
128
+ element.parentElement
122
129
  );
123
130
  }
124
131
 
@@ -218,18 +225,65 @@ function validateMultiple(
218
225
  return element.selectedOptions.length > 0;
219
226
  }
220
227
 
221
- function validateSelectMin(element: ValidatorElement, min: unknown): boolean {
222
- if (!(element instanceof HTMLSelectElement) || typeof min !== 'number') {
223
- return true;
228
+ function checkboxGroup(
229
+ root: HTMLFormElement | null,
230
+ element: ValidatorElement
231
+ ): HTMLInputElement[] {
232
+ if (
233
+ !root ||
234
+ !(element instanceof HTMLInputElement) ||
235
+ element.type !== 'checkbox'
236
+ ) {
237
+ return [];
224
238
  }
225
- return element.selectedOptions.length >= min;
239
+ const controls = Array.from(root.elements).filter(
240
+ (control): control is HTMLInputElement =>
241
+ control instanceof HTMLInputElement &&
242
+ control.type === 'checkbox' &&
243
+ control.name === element.name &&
244
+ !control.closest('[data-field-switch]')
245
+ );
246
+ const isGrouped =
247
+ controls.length > 1 ||
248
+ !!element.closest(
249
+ '[data-choice-type="checkbox"], [data-field-choice-group]'
250
+ );
251
+ return isGrouped ? controls : [];
226
252
  }
227
253
 
228
- function validateSelectMax(element: ValidatorElement, max: unknown): boolean {
229
- if (!(element instanceof HTMLSelectElement) || typeof max !== 'number') {
230
- return true;
254
+ function checkedCount(
255
+ root: HTMLFormElement | null,
256
+ element: ValidatorElement
257
+ ): number | null {
258
+ const controls = checkboxGroup(root, element);
259
+ if (controls.length === 0) return null;
260
+ return controls.filter((control) => control.checked).length;
261
+ }
262
+
263
+ function validateChoiceMin(
264
+ root: HTMLFormElement | null,
265
+ element: ValidatorElement,
266
+ min: unknown
267
+ ): boolean {
268
+ if (typeof min !== 'number') return true;
269
+ if (element instanceof HTMLSelectElement) {
270
+ return element.selectedOptions.length >= min;
231
271
  }
232
- return element.selectedOptions.length <= max;
272
+ const count = checkedCount(root, element);
273
+ return count === null ? true : count >= min;
274
+ }
275
+
276
+ function validateChoiceMax(
277
+ root: HTMLFormElement | null,
278
+ element: ValidatorElement,
279
+ max: unknown
280
+ ): boolean {
281
+ if (typeof max !== 'number') return true;
282
+ if (element instanceof HTMLSelectElement) {
283
+ return element.selectedOptions.length <= max;
284
+ }
285
+ const count = checkedCount(root, element);
286
+ return count === null ? true : count <= max;
233
287
  }
234
288
 
235
289
  function validateNoSpace(element: ValidatorElement, noSpace: unknown): boolean {
@@ -336,7 +390,6 @@ function showSuccess(element: ValidatorElement): void {
336
390
 
337
391
  if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
338
392
  element.classList.remove('is-invalid');
339
- element.classList.add('is-valid');
340
393
  }
341
394
  }
342
395
 
@@ -353,10 +406,17 @@ export function createValidator(
353
406
 
354
407
  let root: HTMLFormElement | null = resolved;
355
408
  let options: ResolvedValidatorProps | null = normalizeProps(props);
409
+ const initialNoValidate = root.noValidate;
356
410
  const runtime = createRuntime();
357
411
  const events = createEventManager();
412
+ const errorNames = new Set<string>();
358
413
  let validator: ValidatorInstance;
359
414
 
415
+ const syncNativeValidation = (): void => {
416
+ if (!root) return;
417
+ root.noValidate = options?.vanilla === false ? true : false;
418
+ };
419
+
360
420
  const validateCustom = (
361
421
  control: ValidatorElement,
362
422
  rule: unknown
@@ -398,10 +458,10 @@ export function createValidator(
398
458
  runtime.valid = validateMultiple(control, value);
399
459
  break;
400
460
  case 'min':
401
- runtime.valid = validateSelectMin(control, value);
461
+ runtime.valid = validateChoiceMin(root, control, value);
402
462
  break;
403
463
  case 'max':
404
- runtime.valid = validateSelectMax(control, value);
464
+ runtime.valid = validateChoiceMax(root, control, value);
405
465
  break;
406
466
  case 'noSpace':
407
467
  runtime.valid = validateNoSpace(control, value);
@@ -435,17 +495,33 @@ export function createValidator(
435
495
  }
436
496
  }
437
497
  if (!runtime.valid) {
498
+ runtime.error = true;
499
+ errorNames.add(name);
438
500
  showError(options, runtime, control, name, rule, customMessage);
439
501
  break;
440
502
  }
441
503
  showSuccess(control);
442
504
  }
505
+ if (runtime.valid) errorNames.delete(name);
443
506
  return runtime.valid;
444
507
  };
508
+ const validateControl = (control: ValidatorElement): boolean => {
509
+ if (!options || runtime.destroyed || !control.name) return true;
510
+ if (!options.rules[control.name]) return true;
511
+ const valid = validateRule(control, control.name);
512
+ if (valid && errorNames.size === 0) {
513
+ runtime.valid = true;
514
+ runtime.error = false;
515
+ runtime.message = '';
516
+ }
517
+ return valid;
518
+ };
445
519
  const validate = (): boolean => {
446
520
  if (!root || !options || runtime.destroyed) return false;
447
521
  runtime.valid = true;
522
+ runtime.error = false;
448
523
  runtime.message = '';
524
+ errorNames.clear();
449
525
  for (const control of Array.from(root.elements)) {
450
526
  if (!isValidatorElement(control) || !control.name) continue;
451
527
  if (!options.rules[control.name]) continue;
@@ -455,6 +531,13 @@ export function createValidator(
455
531
  if (runtime.valid) options.onSubmit?.(validator);
456
532
  return runtime.valid;
457
533
  };
534
+ const handleFieldInput = (event: Event): void => {
535
+ if (!runtime.error) return;
536
+ const target = event.target;
537
+ if (!(target instanceof Element) || !isValidatorElement(target)) return;
538
+ if (!target.name || !errorNames.has(target.name)) return;
539
+ validateControl(target);
540
+ };
458
541
  const reset = ({ native = true }: ResetOptions = {}): void => {
459
542
  if (!root) return;
460
543
  if (native) root.reset();
@@ -467,17 +550,22 @@ export function createValidator(
467
550
  help.remove();
468
551
  }
469
552
  runtime.valid = true;
553
+ runtime.error = false;
470
554
  runtime.message = '';
555
+ errorNames.clear();
471
556
  };
472
557
  const destroy = (): void => {
473
558
  if (runtime.destroyed) return;
474
559
  runtime.destroyed = true;
475
560
  events.clear();
476
561
  reset({ native: false });
562
+ if (root) root.noValidate = initialNoValidate;
477
563
  root = null;
478
564
  options = null;
479
565
  runtime.valid = false;
566
+ runtime.error = false;
480
567
  runtime.message = '';
568
+ errorNames.clear();
481
569
  };
482
570
 
483
571
  validator = {
@@ -489,6 +577,7 @@ export function createValidator(
489
577
  },
490
578
  set props(value) {
491
579
  options = value;
580
+ syncNativeValidation();
492
581
  },
493
582
  runtime,
494
583
  validate,
@@ -503,5 +592,8 @@ export function createValidator(
503
592
  });
504
593
  events.on('reset', root, 'reset', () => reset({ native: false }));
505
594
  }
595
+ events.on('input', root, 'input', handleFieldInput);
596
+ events.on('change', root, 'change', handleFieldInput);
597
+ syncNativeValidation();
506
598
  return validator;
507
599
  }