vanilla-jui 1.4.8 → 1.5.2

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.
Files changed (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -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 { type IEventManager, createEventManager } from '../utilities/events.ts';
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
- dom: ValidatorDOM;
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
- * 支持绑定表单 submit/reset 事件,也可以手动调用 validate/reset。
153
- */
154
- class Validator implements ValidatorInstance {
155
- dom: ValidatorDOM;
156
- props: ResolvedValidatorProps | null;
157
- runtime: ValidatorRuntime;
158
- cleanup: ValidatorCleanup | null;
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
- constructor(
161
- element: DOMReference,
162
- props: ValidatorProps = {},
163
- bindEvents = false
164
- ) {
165
- this.props = normalizeProps(props);
166
- this.dom = {
167
- root: this.resolveRoot(element),
168
- };
169
- this.runtime = createRuntime();
170
- this.cleanup = {
171
- events: createEventManager(),
172
- };
173
-
174
- this.validateOptions(bindEvents);
175
-
176
- if (bindEvents) {
177
- this.bindEvents();
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
- private resolveRoot(element: DOMReference): HTMLFormElement {
182
- const root = resolveElement(element);
183
- if (!(root instanceof HTMLFormElement)) {
184
- throw new Error('Validator.element expects a form element.');
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
- private validateOptions(bindEvents: boolean): void {
190
- validateParam('bindEvents', bindEvents, 'boolean', 'Validator');
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
- private bindEvents(): void {
194
- this.unbindEvents();
195
- const { root } = this.dom;
196
- if (!root || !this.cleanup) return;
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
- this.cleanup.events.on('submit', root, 'submit', (event) => {
199
- event.preventDefault();
200
- this.validate();
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
- private unbindEvents(): void {
208
- this.cleanup?.events.clear();
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
- this.runtime.valid = true;
219
- this.runtime.message = '';
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
- for (const element of Array.from(root.elements)) {
222
- if (!isValidatorElement(element) || !element.name) continue;
223
- if (!this.props.rules[element.name]) continue;
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
- this.runtime.valid = this.validateRule(element, element.name);
226
- if (!this.runtime.valid) break;
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
- if (this.runtime.valid && this.props.onSubmit) {
230
- this.props.onSubmit(this);
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 this.runtime.valid;
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
- private validateRule(element: ValidatorElement, name: string): boolean {
236
- if (!this.props) return false;
237
- const rules = this.props.rules[name];
238
- if (!rules) return true;
307
+ const error = customMessage || props?.messages[name]?.[rule] || '';
308
+ if (!error) return;
309
+
310
+ runtime.message = error;
239
311
 
240
- if (this.hasNativeValidationRule(element)) {
241
- return this.runtime.valid;
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
- this.runtime.valid = this.validateRequired(element, value);
377
+ runtime.valid = validateRequired(control, value);
251
378
  break;
252
379
  case 'minLength':
253
- this.runtime.valid = this.validateMinLength(element, value);
380
+ runtime.valid = validateMinLength(control, value);
254
381
  break;
255
382
  case 'maxLength':
256
- this.runtime.valid = this.validateMaxLength(element, value);
383
+ runtime.valid = validateMaxLength(control, value);
257
384
  break;
258
385
  case 'equalTo':
259
- this.runtime.valid = this.validateEqualTo(element, value);
386
+ runtime.valid = validateEqualTo(root, control, value);
260
387
  break;
261
388
  case 'email':
262
- this.runtime.valid = this.validateEmail(element);
389
+ runtime.valid = validateEmail(control);
263
390
  break;
264
391
  case 'checked':
265
- this.runtime.valid = this.validateCheck(element, value);
392
+ runtime.valid = validateCheck(control, value);
266
393
  break;
267
394
  case 'selected':
268
- this.runtime.valid = this.validateSelected(element, value);
395
+ runtime.valid = validateSelected(control, value);
269
396
  break;
270
397
  case 'multiple':
271
- this.runtime.valid = this.validateMultiple(element, value);
398
+ runtime.valid = validateMultiple(control, value);
272
399
  break;
273
400
  case 'min':
274
- this.runtime.valid = this.validateSelectMin(element, value);
401
+ runtime.valid = validateSelectMin(control, value);
275
402
  break;
276
403
  case 'max':
277
- this.runtime.valid = this.validateSelectMax(element, value);
404
+ runtime.valid = validateSelectMax(control, value);
278
405
  break;
279
406
  case 'noSpace':
280
- this.runtime.valid = this.validateNoSpace(element, value);
407
+ runtime.valid = validateNoSpace(control, value);
281
408
  break;
282
409
  case 'noChinese':
283
- this.runtime.valid = !/[\u4e00-\u9fa5]/.test(element.value);
410
+ runtime.valid = !/[\u4e00-\u9fa5]/.test(control.value);
284
411
  break;
285
412
  case 'noSpecial':
286
- this.runtime.valid = !/[@#$%^&*]+/g.test(element.value);
413
+ runtime.valid = !/[@#$%^&*]+/g.test(control.value);
287
414
  break;
288
415
  case 'pattern':
289
- this.runtime.valid = this.validatePattern(element, value);
416
+ runtime.valid = validatePattern(control, value);
290
417
  break;
291
418
  case 'file':
292
- this.runtime.valid = this.validateFile(element, value);
419
+ runtime.valid = validateFile(control, value);
293
420
  break;
294
421
  case 'minSize':
295
- this.runtime.valid = this.validateMinSize(element, value);
422
+ runtime.valid = validateMinSize(control, value);
296
423
  break;
297
424
  case 'maxSize':
298
- this.runtime.valid = this.validateMaxSize(element, value);
425
+ runtime.valid = validateMaxSize(control, value);
299
426
  break;
300
427
  case 'accept':
301
- this.runtime.valid = this.validateAccept(element, value);
428
+ runtime.valid = validateAccept(control, value);
302
429
  break;
303
430
  case 'validate': {
304
- const result = this.validateCustom(element, value);
305
- this.runtime.valid = result === true;
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
- if (!this.runtime.valid) {
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
- if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
556
- element.classList.remove('is-invalid');
557
- element.classList.add('is-valid');
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
- for (const element of Array.from(root.elements)) {
570
- if (!isValidatorElement(element)) continue;
571
- element.classList.remove('is-valid');
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
- this.runtime.valid = true;
580
- this.runtime.message = '';
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
- destroy(): void {
587
- if (this.runtime.destroyed) return;
588
- this.runtime.destroyed = true;
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
  }