vanilla-jui 1.3.11 → 1.4.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.
Files changed (71) hide show
  1. package/dist/index.d.ts +2927 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +4 -3
  6. package/src/components/accordion.ts +522 -0
  7. package/src/components/drop.ts +497 -0
  8. package/src/components/flow.ts +1579 -0
  9. package/src/components/form.ts +953 -0
  10. package/src/components/{icons.js → icons.ts} +24 -11
  11. package/src/components/menu.ts +615 -0
  12. package/src/components/{modal.js → modal.ts} +560 -237
  13. package/src/components/offcanvas.ts +496 -0
  14. package/src/components/{pagination.js → pagination.ts} +251 -76
  15. package/src/components/parabola.ts +335 -0
  16. package/src/components/{sticky.js → sticky.ts} +123 -98
  17. package/src/components/{swiper.js → swiper.ts} +510 -232
  18. package/src/components/tabs.ts +984 -0
  19. package/src/components/theme.ts +437 -0
  20. package/src/components/toast.ts +371 -0
  21. package/src/components/toc.ts +385 -0
  22. package/src/components/tooltip.ts +179 -0
  23. package/src/components/validator.ts +596 -0
  24. package/src/core/{Component.js → Component.ts} +134 -27
  25. package/src/css/animation.css +31 -1
  26. package/src/css/card.css +1 -1
  27. package/src/css/color.css +863 -1013
  28. package/src/css/color_p3.css +822 -975
  29. package/src/css/components.css +1 -0
  30. package/src/css/form.css +1 -1
  31. package/src/css/icon.css +17 -1
  32. package/src/css/index.css +2 -2
  33. package/src/css/menu.css +1 -1
  34. package/src/css/tabs.css +1 -0
  35. package/src/css/tokens.css +1 -1
  36. package/src/index.ts +37 -0
  37. package/src/locales/{index.js → index.ts} +1 -2
  38. package/src/types.d.ts +1 -0
  39. package/src/utilities/browser.ts +131 -0
  40. package/src/utilities/cache.ts +57 -0
  41. package/src/utilities/core.ts +614 -0
  42. package/src/utilities/dom.ts +468 -0
  43. package/src/utilities/events.ts +106 -0
  44. package/src/utilities/http.ts +35 -0
  45. package/src/utilities/storage.ts +46 -0
  46. package/src/components/accordion.js +0 -373
  47. package/src/components/drop.js +0 -437
  48. package/src/components/flow.js +0 -1133
  49. package/src/components/form.js +0 -585
  50. package/src/components/menu.js +0 -422
  51. package/src/components/offcanvas.js +0 -332
  52. package/src/components/parabola.js +0 -295
  53. package/src/components/tabs.js +0 -746
  54. package/src/components/theme.js +0 -353
  55. package/src/components/toast.js +0 -329
  56. package/src/components/toc.js +0 -226
  57. package/src/components/tooltip.js +0 -86
  58. package/src/components/validator.js +0 -586
  59. package/src/index.js +0 -45
  60. package/src/locales/index.min.js +0 -1
  61. package/src/utilities/browser.js +0 -111
  62. package/src/utilities/cache.js +0 -159
  63. package/src/utilities/core.js +0 -502
  64. package/src/utilities/debug.js +0 -103
  65. package/src/utilities/dom.js +0 -313
  66. package/src/utilities/events.js +0 -69
  67. package/src/utilities/http.js +0 -30
  68. package/src/utilities/legacy-dom.js +0 -179
  69. package/src/utilities/storage.js +0 -46
  70. /package/src/{components/components.js → css/themes/index.css} +0 -0
  71. /package/src/icons/{index.js → index.ts} +0 -0
@@ -0,0 +1,596 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import {
4
+ type ResolveSchema,
5
+ resolveProps,
6
+ validateParam,
7
+ } from '../utilities/core.ts';
8
+ import { type DOMReference, all, resolveElement } from '../utilities/dom.ts';
9
+ import { type IEventManager, createEventManager } from '../utilities/events.ts';
10
+
11
+ type ValidatorElement =
12
+ | HTMLInputElement
13
+ | HTMLSelectElement
14
+ | HTMLTextAreaElement;
15
+ type ValidatorRuleName = keyof ValidatorRule | string;
16
+ type ValidatorMessageMap = Record<
17
+ string,
18
+ Partial<Record<ValidatorRuleName, string>>
19
+ >;
20
+ type ValidatorCustomResult = boolean | string;
21
+ type ValidatorCustomRule = (
22
+ element: ValidatorElement,
23
+ validator: Validator
24
+ ) => ValidatorCustomResult;
25
+
26
+ export interface ValidatorRule extends Record<string, unknown> {
27
+ required?: boolean;
28
+ minLength?: number;
29
+ maxLength?: number;
30
+ equalTo?: string;
31
+ email?: boolean;
32
+ checked?: boolean;
33
+ selected?: boolean;
34
+ multiple?: boolean;
35
+ min?: number;
36
+ max?: number;
37
+ noSpace?: boolean;
38
+ noChinese?: boolean;
39
+ noSpecial?: boolean;
40
+ pattern?: string | RegExp;
41
+ file?: boolean;
42
+ minSize?: number;
43
+ maxSize?: number;
44
+ accept?: string;
45
+ validate?: ValidatorCustomRule;
46
+ }
47
+
48
+ export interface ValidatorOptions extends Record<string, unknown> {
49
+ rules?: Record<string, ValidatorRule>;
50
+ messages?: ValidatorMessageMap;
51
+ onSubmit?: ((validator: Validator) => void) | null;
52
+ }
53
+
54
+ interface ResolvedValidatorOptions extends Record<string, unknown> {
55
+ rules: Record<string, ValidatorRule>;
56
+ messages: ValidatorMessageMap;
57
+ onSubmit: ((validator: Validator) => void) | null;
58
+ }
59
+
60
+ interface ValidatorRuntime {
61
+ valid: boolean;
62
+ message: string;
63
+ destroyed: boolean;
64
+ }
65
+
66
+ interface ValidatorCleanup {
67
+ events: IEventManager;
68
+ }
69
+
70
+ interface ResetOptions {
71
+ native?: boolean;
72
+ }
73
+
74
+ const VALIDATOR_OPTIONS_SCHEMA = {
75
+ rules: { default: {}, type: 'object' },
76
+ messages: { default: {}, type: 'object' },
77
+ onSubmit: { default: null, types: ['function', 'null'] },
78
+ } satisfies ResolveSchema<ValidatorOptions>;
79
+
80
+ function normalizeOptions(input: ValidatorOptions): ResolvedValidatorOptions {
81
+ const options = resolveProps(
82
+ input,
83
+ VALIDATOR_OPTIONS_SCHEMA,
84
+ 'Validator.options'
85
+ );
86
+ return {
87
+ rules: options.rules as Record<string, ValidatorRule>,
88
+ messages: options.messages as ValidatorMessageMap,
89
+ onSubmit: options.onSubmit as ResolvedValidatorOptions['onSubmit'],
90
+ };
91
+ }
92
+
93
+ function isValidatorElement(element: Element): element is ValidatorElement {
94
+ return (
95
+ element instanceof HTMLInputElement ||
96
+ element instanceof HTMLSelectElement ||
97
+ element instanceof HTMLTextAreaElement
98
+ );
99
+ }
100
+
101
+ function toValidatorElement(
102
+ value: Element | RadioNodeList | null
103
+ ): ValidatorElement | null {
104
+ if (!value) return null;
105
+ if (value instanceof Element) {
106
+ return isValidatorElement(value) ? value : null;
107
+ }
108
+ for (const item of Array.from(value)) {
109
+ if (item instanceof Element && isValidatorElement(item)) return item;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ function createRuntime(): ValidatorRuntime {
115
+ return {
116
+ valid: true,
117
+ message: '',
118
+ destroyed: false,
119
+ };
120
+ }
121
+
122
+ function getControlContainer(element: ValidatorElement): HTMLElement | null {
123
+ return (
124
+ element.closest<HTMLElement>('[data-form-control]') || element.parentElement
125
+ );
126
+ }
127
+
128
+ function selectedValues(element: HTMLSelectElement): string[] {
129
+ return Array.from(element.selectedOptions).map((option) =>
130
+ option.value.trim()
131
+ );
132
+ }
133
+
134
+ function isFileInput(element: ValidatorElement): element is HTMLInputElement {
135
+ return element instanceof HTMLInputElement && element.type === 'file';
136
+ }
137
+
138
+ function hasFiles(element: HTMLInputElement): element is HTMLInputElement & {
139
+ files: FileList;
140
+ } {
141
+ return !!element.files && element.files.length > 0;
142
+ }
143
+
144
+ /**
145
+ * 表单校验组件。
146
+ *
147
+ * 支持绑定表单 submit/reset 事件,也可以手动调用 validate/reset。
148
+ */
149
+ export class Validator {
150
+ root: HTMLFormElement | null;
151
+ options: ResolvedValidatorOptions | null;
152
+ runtime: ValidatorRuntime;
153
+ cleanup: ValidatorCleanup | null;
154
+
155
+ constructor(
156
+ element: DOMReference,
157
+ options: ValidatorOptions = {},
158
+ bindEvents = false
159
+ ) {
160
+ this.options = normalizeOptions(options);
161
+ this.root = this.resolveRoot(element);
162
+ this.runtime = createRuntime();
163
+ this.cleanup = {
164
+ events: createEventManager(),
165
+ };
166
+
167
+ this.validateOptions(bindEvents);
168
+
169
+ if (bindEvents) {
170
+ this.bindEvents();
171
+ }
172
+ }
173
+
174
+ private resolveRoot(element: DOMReference): HTMLFormElement {
175
+ const root = resolveElement(element);
176
+ if (!(root instanceof HTMLFormElement)) {
177
+ throw new Error('Validator.element expects a form element.');
178
+ }
179
+ return root;
180
+ }
181
+
182
+ private validateOptions(bindEvents: boolean): void {
183
+ validateParam('bindEvents', bindEvents, 'boolean', 'Validator');
184
+ }
185
+
186
+ private bindEvents(): void {
187
+ this.unbindEvents();
188
+ if (!this.root || !this.cleanup) return;
189
+
190
+ this.cleanup.events.on('submit', this.root, 'submit', (event) => {
191
+ event.preventDefault();
192
+ this.validate();
193
+ });
194
+ this.cleanup.events.on('reset', this.root, 'reset', () => {
195
+ this.reset({ native: false });
196
+ });
197
+ }
198
+
199
+ private unbindEvents(): void {
200
+ this.cleanup?.events.clear();
201
+ }
202
+
203
+ /**
204
+ * 执行表单校验。
205
+ */
206
+ validate(): boolean {
207
+ if (!this.root || !this.options) return false;
208
+
209
+ this.runtime.valid = true;
210
+ this.runtime.message = '';
211
+
212
+ for (const element of Array.from(this.root.elements)) {
213
+ if (!isValidatorElement(element) || !element.name) continue;
214
+ if (!this.options.rules[element.name]) continue;
215
+
216
+ this.runtime.valid = this.validateRule(element, element.name);
217
+ if (!this.runtime.valid) break;
218
+ }
219
+
220
+ if (this.runtime.valid && this.options.onSubmit) {
221
+ this.options.onSubmit(this);
222
+ }
223
+ return this.runtime.valid;
224
+ }
225
+
226
+ private validateRule(element: ValidatorElement, name: string): boolean {
227
+ if (!this.options) return false;
228
+ const rules = this.options.rules[name];
229
+ if (!rules) return true;
230
+
231
+ if (this.hasNativeValidationRule(element)) {
232
+ return this.runtime.valid;
233
+ }
234
+
235
+ for (const rule of Object.keys(rules)) {
236
+ const value = rules[rule];
237
+ let customMessage = '';
238
+
239
+ switch (rule) {
240
+ case 'required':
241
+ this.runtime.valid = this.validateRequired(element, value);
242
+ break;
243
+ case 'minLength':
244
+ this.runtime.valid = this.validateMinLength(element, value);
245
+ break;
246
+ case 'maxLength':
247
+ this.runtime.valid = this.validateMaxLength(element, value);
248
+ break;
249
+ case 'equalTo':
250
+ this.runtime.valid = this.validateEqualTo(element, value);
251
+ break;
252
+ case 'email':
253
+ this.runtime.valid = this.validateEmail(element);
254
+ break;
255
+ case 'checked':
256
+ this.runtime.valid = this.validateCheck(element, value);
257
+ break;
258
+ case 'selected':
259
+ this.runtime.valid = this.validateSelected(element, value);
260
+ break;
261
+ case 'multiple':
262
+ this.runtime.valid = this.validateMultiple(element, value);
263
+ break;
264
+ case 'min':
265
+ this.runtime.valid = this.validateSelectMin(element, value);
266
+ break;
267
+ case 'max':
268
+ this.runtime.valid = this.validateSelectMax(element, value);
269
+ break;
270
+ case 'noSpace':
271
+ this.runtime.valid = this.validateNoSpace(element, value);
272
+ break;
273
+ case 'noChinese':
274
+ this.runtime.valid = !/[\u4e00-\u9fa5]/.test(element.value);
275
+ break;
276
+ case 'noSpecial':
277
+ this.runtime.valid = !/[@#$%^&*]+/g.test(element.value);
278
+ break;
279
+ case 'pattern':
280
+ this.runtime.valid = this.validatePattern(element, value);
281
+ break;
282
+ case 'file':
283
+ this.runtime.valid = this.validateFile(element, value);
284
+ break;
285
+ case 'minSize':
286
+ this.runtime.valid = this.validateMinSize(element, value);
287
+ break;
288
+ case 'maxSize':
289
+ this.runtime.valid = this.validateMaxSize(element, value);
290
+ break;
291
+ case 'accept':
292
+ this.runtime.valid = this.validateAccept(element, value);
293
+ break;
294
+ case 'validate': {
295
+ const result = this.validateCustom(element, value);
296
+ this.runtime.valid = result === true;
297
+ customMessage = typeof result === 'string' ? result : '';
298
+ break;
299
+ }
300
+ }
301
+
302
+ if (!this.runtime.valid) {
303
+ this.showError(element, name, rule, customMessage);
304
+ break;
305
+ }
306
+
307
+ this.showSuccess(element);
308
+ }
309
+
310
+ return this.runtime.valid;
311
+ }
312
+
313
+ private hasNativeValidationRule(element: ValidatorElement): boolean {
314
+ return (
315
+ element.hasAttribute('required') ||
316
+ element.hasAttribute('minlength') ||
317
+ element.hasAttribute('maxlength') ||
318
+ element.hasAttribute('pattern') ||
319
+ element.hasAttribute('min') ||
320
+ element.hasAttribute('max') ||
321
+ element.hasAttribute('step')
322
+ );
323
+ }
324
+
325
+ private validateRequired(
326
+ element: ValidatorElement,
327
+ required: unknown
328
+ ): boolean {
329
+ if (required !== true) return true;
330
+ if (element instanceof HTMLSelectElement) {
331
+ return selectedValues(element).some((value) => value.length > 0);
332
+ }
333
+ return element.value.trim().length >= 1;
334
+ }
335
+
336
+ private validateMinLength(
337
+ element: ValidatorElement,
338
+ minLength: unknown
339
+ ): boolean {
340
+ if (typeof minLength !== 'number') return true;
341
+ return element.value.length >= minLength;
342
+ }
343
+
344
+ private validateMaxLength(
345
+ element: ValidatorElement,
346
+ maxLength: unknown
347
+ ): boolean {
348
+ if (typeof maxLength !== 'number') return true;
349
+ return element.value.length <= maxLength;
350
+ }
351
+
352
+ private validateEmail(element: ValidatorElement): boolean {
353
+ const emailPattern = /^([\w-.]+@([\w-]+\.)+[\w-]{2,4})?$/;
354
+ return emailPattern.test(element.value);
355
+ }
356
+
357
+ private validateEqualTo(
358
+ element: ValidatorElement,
359
+ targetName: unknown
360
+ ): boolean {
361
+ if (!this.root || typeof targetName !== 'string') return true;
362
+ const targetElement = toValidatorElement(
363
+ this.root.elements.namedItem(targetName)
364
+ );
365
+ if (!targetElement) {
366
+ throw new Error(`Validator: target element "${targetName}" not found.`);
367
+ }
368
+ return element.value === targetElement.value;
369
+ }
370
+
371
+ private validateCheck(element: ValidatorElement, checked: unknown): boolean {
372
+ if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
373
+ throw new Error(
374
+ `Validator: element expects a checkbox input, but ${element.tagName.toLowerCase()} given.`
375
+ );
376
+ }
377
+ return element.checked === checked;
378
+ }
379
+
380
+ private validateSelected(
381
+ element: ValidatorElement,
382
+ selected: unknown
383
+ ): boolean {
384
+ if (!(element instanceof HTMLSelectElement)) {
385
+ throw new Error(
386
+ `Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
387
+ );
388
+ }
389
+ if (selected !== true) return true;
390
+ return selectedValues(element).some((value) => value.length > 0);
391
+ }
392
+
393
+ private validateMultiple(
394
+ element: ValidatorElement,
395
+ multiple: unknown
396
+ ): boolean {
397
+ if (!(element instanceof HTMLSelectElement)) {
398
+ throw new Error(
399
+ `Validator: element expects a select element, but ${element.tagName.toLowerCase()} given.`
400
+ );
401
+ }
402
+ if (multiple !== true) return true;
403
+ return element.selectedOptions.length > 0;
404
+ }
405
+
406
+ private validateSelectMin(element: ValidatorElement, min: unknown): boolean {
407
+ if (!(element instanceof HTMLSelectElement) || typeof min !== 'number') {
408
+ return true;
409
+ }
410
+ return element.selectedOptions.length >= min;
411
+ }
412
+
413
+ private validateSelectMax(element: ValidatorElement, max: unknown): boolean {
414
+ if (!(element instanceof HTMLSelectElement) || typeof max !== 'number') {
415
+ return true;
416
+ }
417
+ return element.selectedOptions.length <= max;
418
+ }
419
+
420
+ private validateNoSpace(
421
+ element: ValidatorElement,
422
+ noSpace: unknown
423
+ ): boolean {
424
+ return !/\s/.test(element.value) || noSpace !== true;
425
+ }
426
+
427
+ private validatePattern(
428
+ element: ValidatorElement,
429
+ pattern: unknown
430
+ ): boolean {
431
+ if (!(typeof pattern === 'string' || pattern instanceof RegExp))
432
+ return true;
433
+ return new RegExp(pattern).test(element.value);
434
+ }
435
+
436
+ private validateFile(element: ValidatorElement, required: unknown): boolean {
437
+ if (!isFileInput(element)) return true;
438
+ return required === true ? hasFiles(element) : true;
439
+ }
440
+
441
+ private validateMinSize(
442
+ element: ValidatorElement,
443
+ minSize: unknown
444
+ ): boolean {
445
+ if (
446
+ !isFileInput(element) ||
447
+ !hasFiles(element) ||
448
+ typeof minSize !== 'number'
449
+ ) {
450
+ return true;
451
+ }
452
+ return element.files[0].size >= minSize;
453
+ }
454
+
455
+ private validateMaxSize(
456
+ element: ValidatorElement,
457
+ maxSize: unknown
458
+ ): boolean {
459
+ if (
460
+ !isFileInput(element) ||
461
+ !hasFiles(element) ||
462
+ typeof maxSize !== 'number'
463
+ ) {
464
+ return true;
465
+ }
466
+ return element.files[0].size <= maxSize;
467
+ }
468
+
469
+ private validateAccept(element: ValidatorElement, accept: unknown): boolean {
470
+ if (
471
+ !isFileInput(element) ||
472
+ !hasFiles(element) ||
473
+ typeof accept !== 'string'
474
+ ) {
475
+ return true;
476
+ }
477
+
478
+ const file = element.files[0];
479
+ const allowed = accept.split(',').map((item) => item.trim().toLowerCase());
480
+
481
+ return allowed.some((rule) => {
482
+ if (rule.startsWith('.')) {
483
+ return file.name.toLowerCase().endsWith(rule);
484
+ }
485
+ if (rule.endsWith('/*')) {
486
+ return file.type.startsWith(rule.replace('/*', '/'));
487
+ }
488
+ return file.type === rule;
489
+ });
490
+ }
491
+
492
+ private validateCustom(
493
+ element: ValidatorElement,
494
+ rule: unknown
495
+ ): ValidatorCustomResult {
496
+ if (typeof rule !== 'function') return true;
497
+ const result = (rule as ValidatorCustomRule)(element, this);
498
+ return typeof result === 'string' ? result : !!result;
499
+ }
500
+
501
+ private showError(
502
+ element: ValidatorElement,
503
+ name: string,
504
+ rule: string,
505
+ customMessage = ''
506
+ ): void {
507
+ if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
508
+ element.classList.remove('is-valid');
509
+ element.classList.add('is-invalid');
510
+ }
511
+
512
+ const error = customMessage || this.options?.messages[name]?.[rule] || '';
513
+ if (!error) return;
514
+
515
+ this.runtime.message = error;
516
+
517
+ const formControl = getControlContainer(element);
518
+ if (!formControl) return;
519
+
520
+ let help =
521
+ all<HTMLElement>('[data-validator-help]', formControl).find(
522
+ (element) => element.dataset.validatorHelp === name
523
+ ) || null;
524
+ if (!help) {
525
+ help = jsx('div', {
526
+ className: 'help-block is-invalid',
527
+ 'data-validator-help': name,
528
+ }) as HTMLElement;
529
+ formControl.appendChild(help);
530
+ }
531
+ help.textContent = error;
532
+ }
533
+
534
+ private showSuccess(element: ValidatorElement): void {
535
+ const formControl = getControlContainer(element);
536
+ if (formControl) {
537
+ for (const help of all<HTMLElement>(
538
+ '[data-validator-help]',
539
+ formControl
540
+ )) {
541
+ if (help.dataset.validatorHelp === element.name) help.remove();
542
+ }
543
+ }
544
+
545
+ if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
546
+ element.classList.remove('is-invalid');
547
+ element.classList.add('is-valid');
548
+ }
549
+ }
550
+
551
+ /**
552
+ * 重置表单与校验状态。
553
+ */
554
+ reset({ native = true }: ResetOptions = {}): void {
555
+ if (!this.root) return;
556
+ if (native) this.root.reset();
557
+
558
+ for (const element of Array.from(this.root.elements)) {
559
+ if (!isValidatorElement(element)) continue;
560
+ element.classList.remove('is-valid');
561
+ element.classList.remove('is-invalid');
562
+ }
563
+
564
+ for (const help of all<HTMLElement>('[data-validator-help]', this.root)) {
565
+ help.remove();
566
+ }
567
+
568
+ this.runtime.valid = true;
569
+ this.runtime.message = '';
570
+ }
571
+
572
+ /**
573
+ * 销毁当前校验实例。
574
+ */
575
+ destroy(): void {
576
+ if (this.runtime.destroyed) return;
577
+ this.runtime.destroyed = true;
578
+
579
+ this.unbindEvents();
580
+ this.reset({ native: false });
581
+ this.root = null;
582
+ this.options = null;
583
+ this.runtime.valid = false;
584
+ this.runtime.message = '';
585
+ this.cleanup?.events.clear();
586
+ this.cleanup = null;
587
+ }
588
+ }
589
+
590
+ export function createValidator(
591
+ element: DOMReference,
592
+ options: ValidatorOptions = {},
593
+ bindEvents = false
594
+ ): Validator {
595
+ return new Validator(element, options, bindEvents);
596
+ }