vanilla-jui 1.4.5 → 1.4.7

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 (103) hide show
  1. package/README.md +6 -0
  2. package/dist/index.d.ts +1273 -1896
  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 +21 -2
  7. package/src/components/accordion.ts +297 -216
  8. package/src/components/flow.ts +222 -258
  9. package/src/components/form.ts +44 -102
  10. package/src/components/menu.ts +268 -356
  11. package/src/components/modal.ts +181 -641
  12. package/src/components/offcanvas.ts +225 -106
  13. package/src/components/pagination.ts +296 -326
  14. package/src/components/sticky.ts +55 -54
  15. package/src/components/swiper.ts +237 -247
  16. package/src/components/tabs.ts +278 -271
  17. package/src/components/toc.ts +42 -46
  18. package/src/core/Component.ts +69 -47
  19. package/src/css/accordion.css +2 -2
  20. package/src/css/animation.css +1 -5
  21. package/src/css/avatar.css +1 -1
  22. package/src/css/badge.css +1 -1
  23. package/src/css/breadcrumb.css +9 -10
  24. package/src/css/button.css +1 -1
  25. package/src/css/card.css +2 -2
  26. package/src/css/color.css +974 -1002
  27. package/src/css/color_p3.css +974 -1004
  28. package/src/css/components.css +1 -1
  29. package/src/css/content.css +72 -63
  30. package/src/css/drop.css +1 -1
  31. package/src/css/editor.css +14 -14
  32. package/src/css/flow.css +4 -14
  33. package/src/css/font.css +5 -4
  34. package/src/css/form.css +76 -52
  35. package/src/css/gradient.css +299 -222
  36. package/src/css/icon.css +57 -57
  37. package/src/css/index.css +1 -1
  38. package/src/css/layout.css +13 -10
  39. package/src/css/loading.css +60 -12
  40. package/src/css/menu.css +262 -188
  41. package/src/css/modal.css +1 -1
  42. package/src/css/offcanvas.css +9 -13
  43. package/src/css/pagination.css +5 -1
  44. package/src/css/popup.css +1 -1
  45. package/src/css/skeleton.css +3 -2
  46. package/src/css/status.css +2 -3
  47. package/src/css/swiper.css +1 -1
  48. package/src/css/table.css +1 -1
  49. package/src/css/tabs.css +12 -11
  50. package/src/css/tag.css +1 -1
  51. package/src/css/theme-palette.css +19 -5
  52. package/src/css/themes/default/accordion.css +61 -61
  53. package/src/css/themes/default/avatar.css +54 -54
  54. package/src/css/themes/default/badge.css +62 -62
  55. package/src/css/themes/default/breadcrumb.css +23 -23
  56. package/src/css/themes/default/button.css +203 -203
  57. package/src/css/themes/default/content.css +53 -53
  58. package/src/css/themes/default/drop.css +10 -10
  59. package/src/css/themes/default/editor.css +9 -9
  60. package/src/css/themes/default/index.css +1 -1
  61. package/src/css/themes/default/loading.css +26 -20
  62. package/src/css/themes/default/menu.css +75 -11
  63. package/src/css/themes/default/modal.css +32 -36
  64. package/src/css/themes/default/pagination.css +7 -7
  65. package/src/css/themes/default/popup.css +6 -6
  66. package/src/css/themes/default/swiper.css +32 -32
  67. package/src/css/themes/default/table.css +16 -16
  68. package/src/css/themes/default/tabs.css +30 -30
  69. package/src/css/themes/default/tag.css +71 -71
  70. package/src/css/themes/default/theme-palette.css +11 -11
  71. package/src/css/themes/default/tip.css +42 -42
  72. package/src/css/themes/default/toast.css +60 -60
  73. package/src/css/themes/default/toolbar.css +23 -29
  74. package/src/css/themes/default/tooltip.css +43 -13
  75. package/src/css/themes/index.css +1 -1
  76. package/src/css/tip.css +2 -2
  77. package/src/css/toast.css +3 -3
  78. package/src/css/tokens.css +12 -17
  79. package/src/css/toolbar.css +9 -10
  80. package/src/css/tooltip.css +3 -2
  81. package/src/index.ts +22 -19
  82. package/src/{components → primitives}/drop.ts +58 -49
  83. package/src/{components → primitives}/icons.ts +1 -1
  84. package/src/primitives/loading.ts +23 -0
  85. package/src/primitives/parabola.ts +388 -0
  86. package/src/primitives/popup.ts +61 -0
  87. package/src/{components → primitives}/theme.ts +89 -57
  88. package/src/{components → primitives}/toast.ts +18 -25
  89. package/src/{components → primitives}/tooltip.ts +100 -25
  90. package/src/utilities/browser.ts +23 -49
  91. package/src/utilities/class-name.ts +5 -0
  92. package/src/utilities/dom.ts +14 -32
  93. package/src/utilities/function.ts +4 -0
  94. package/src/utilities/http.ts +7 -7
  95. package/src/utilities/id.ts +57 -0
  96. package/src/utilities/object.ts +5 -0
  97. package/src/utilities/timer.ts +38 -0
  98. package/src/utilities/types.ts +420 -0
  99. package/src/{components → validation}/validator.ts +64 -53
  100. package/src/components/parabola.ts +0 -335
  101. package/src/utilities/cache.ts +0 -57
  102. package/src/utilities/core.ts +0 -614
  103. package/src/utilities/storage.ts +0 -46
@@ -0,0 +1,420 @@
1
+ import { isPlainObject } from './object.ts';
2
+
3
+ type LooseRecord = Record<string, unknown>;
4
+ type ValueTypeName = string;
5
+ type TypeRule = ValueTypeName | readonly ValueTypeName[];
6
+
7
+ export function isNilValue(value: unknown): value is null | undefined {
8
+ return value == null;
9
+ }
10
+
11
+ export function isDomNodeValue(value: unknown): value is Node {
12
+ return typeof Node !== 'undefined' && value instanceof Node;
13
+ }
14
+
15
+ export function isDomElementValue(value: unknown): value is Element {
16
+ return typeof Element !== 'undefined' && value instanceof Element;
17
+ }
18
+
19
+ export function isHtmlElementValue(value: unknown): value is HTMLElement {
20
+ return typeof HTMLElement !== 'undefined' && value instanceof HTMLElement;
21
+ }
22
+
23
+ export function isRenderablePrimitive(
24
+ value: unknown
25
+ ): value is string | number | boolean {
26
+ return (
27
+ typeof value === 'string' ||
28
+ typeof value === 'number' ||
29
+ typeof value === 'boolean'
30
+ );
31
+ }
32
+
33
+ export function isRenderableValue(value: unknown): boolean {
34
+ return (
35
+ isNilValue(value) ||
36
+ isRenderablePrimitive(value) ||
37
+ typeof value === 'function' ||
38
+ Array.isArray(value) ||
39
+ isDomNodeValue(value)
40
+ );
41
+ }
42
+
43
+ export const getType = (val: unknown): string => {
44
+ if (val === null) return 'null';
45
+ if (Array.isArray(val)) return 'array';
46
+ if (isHtmlElementValue(val)) return 'HTMLElement';
47
+ if (isDomNodeValue(val)) return 'Node';
48
+ return typeof val;
49
+ };
50
+
51
+ export type ValidateCondition =
52
+ | ((value: unknown) => boolean)
53
+ | {
54
+ test: (value: unknown) => boolean;
55
+ message?: string;
56
+ };
57
+
58
+ export interface NormalizeContext<TInput extends LooseRecord = LooseRecord> {
59
+ key: string;
60
+ input: TInput;
61
+ options: LooseRecord;
62
+ schema: ResolveSchema<TInput>;
63
+ }
64
+
65
+ export interface ParamRule<TInput extends LooseRecord = LooseRecord> {
66
+ type?: TypeRule;
67
+ types?: TypeRule;
68
+ required?: boolean;
69
+ enum?: readonly unknown[];
70
+ nonEmpty?: boolean;
71
+ minLength?: number;
72
+ maxLength?: number;
73
+ finite?: boolean;
74
+ integer?: boolean;
75
+ min?: number;
76
+ max?: number;
77
+ greaterThan?: number;
78
+ lessThan?: number;
79
+ plain?: boolean;
80
+ items?: ParamRuleInput;
81
+ shape?: ResolveSchema;
82
+ conditions?: ValidateCondition | readonly ValidateCondition[];
83
+ validate?: (value: unknown) => boolean;
84
+ message?: string;
85
+ normalize?: (value: unknown, context: NormalizeContext<TInput>) => unknown;
86
+ default?: unknown;
87
+ factory?: boolean;
88
+ [key: string]: unknown;
89
+ }
90
+
91
+ export type ParamRuleInput<TInput extends LooseRecord = LooseRecord> =
92
+ | TypeRule
93
+ | ParamRule<TInput>;
94
+
95
+ export type ResolveSchema<TInput extends LooseRecord = LooseRecord> = Record<
96
+ string,
97
+ ParamRuleInput<TInput>
98
+ >;
99
+
100
+ export type ResolvedProps<TSchema extends object> = LooseRecord & {
101
+ [Key in keyof TSchema]: unknown;
102
+ };
103
+
104
+ function cloneDefault<T>(value: T): T {
105
+ if (Array.isArray(value)) return value.slice() as T;
106
+ if (isPlainObject(value)) return { ...(value as LooseRecord) } as T;
107
+ return value;
108
+ }
109
+
110
+ function normalizeRule<TInput extends LooseRecord = LooseRecord>(
111
+ rule: ParamRuleInput<TInput> = {}
112
+ ): ParamRule<TInput> {
113
+ if (typeof rule === 'string' || Array.isArray(rule)) return { type: rule };
114
+ if (!rule || typeof rule !== 'object') return {};
115
+ return rule as ParamRule<TInput>;
116
+ }
117
+
118
+ function throwValidateError(name: string, message: string): never {
119
+ throw new Error(`Validator: ${name} ${message}`);
120
+ }
121
+
122
+ function stringifyExpected(value: unknown): string {
123
+ return typeof value === 'string' ? value : String(value);
124
+ }
125
+
126
+ function describeLengthValue(value: unknown): string {
127
+ return typeof value === 'string' ? 'string' : 'array';
128
+ }
129
+
130
+ function validateLengthRule<TInput extends LooseRecord = LooseRecord>(
131
+ name: string,
132
+ value: unknown,
133
+ rule: ParamRule<TInput>
134
+ ): void {
135
+ if (isNilValue(value)) return;
136
+ if (typeof value !== 'string' && !Array.isArray(value)) return;
137
+
138
+ const label = describeLengthValue(value);
139
+ if (rule.nonEmpty && value.length === 0) {
140
+ throwValidateError(name, `expects a non-empty ${label}.`);
141
+ }
142
+ if (typeof rule.minLength === 'number' && value.length < rule.minLength) {
143
+ throwValidateError(
144
+ name,
145
+ `expects ${label} length to be at least ${rule.minLength}.`
146
+ );
147
+ }
148
+ if (typeof rule.maxLength === 'number' && value.length > rule.maxLength) {
149
+ throwValidateError(
150
+ name,
151
+ `expects ${label} length to be at most ${rule.maxLength}.`
152
+ );
153
+ }
154
+ }
155
+
156
+ function validateNumberRule<TInput extends LooseRecord = LooseRecord>(
157
+ name: string,
158
+ value: unknown,
159
+ rule: ParamRule<TInput>
160
+ ): void {
161
+ if (isNilValue(value) || typeof value !== 'number') return;
162
+ if (rule.finite && !Number.isFinite(value)) {
163
+ throwValidateError(name, 'expects a finite number.');
164
+ }
165
+ if (rule.integer && !Number.isInteger(value)) {
166
+ throwValidateError(name, 'expects an integer.');
167
+ }
168
+ if (typeof rule.min === 'number' && value < rule.min) {
169
+ throwValidateError(
170
+ name,
171
+ `expects a number greater than or equal to ${rule.min}.`
172
+ );
173
+ }
174
+ if (typeof rule.max === 'number' && value > rule.max) {
175
+ throwValidateError(
176
+ name,
177
+ `expects a number less than or equal to ${rule.max}.`
178
+ );
179
+ }
180
+ if (typeof rule.greaterThan === 'number' && value <= rule.greaterThan) {
181
+ throwValidateError(
182
+ name,
183
+ `expects a number greater than ${rule.greaterThan}.`
184
+ );
185
+ }
186
+ if (typeof rule.lessThan === 'number' && value >= rule.lessThan) {
187
+ throwValidateError(name, `expects a number less than ${rule.lessThan}.`);
188
+ }
189
+ }
190
+
191
+ function validatePlainRule<TInput extends LooseRecord = LooseRecord>(
192
+ name: string,
193
+ value: unknown,
194
+ rule: ParamRule<TInput>
195
+ ): void {
196
+ if (!rule.plain || isNilValue(value)) return;
197
+ if (!isPlainObject(value)) {
198
+ throwValidateError(name, 'expects an object.');
199
+ }
200
+ }
201
+
202
+ function validateItemsRule<TInput extends LooseRecord = LooseRecord>(
203
+ name: string,
204
+ value: unknown,
205
+ rule: ParamRule<TInput>
206
+ ): void {
207
+ if (!rule.items || isNilValue(value)) return;
208
+ if (!Array.isArray(value)) {
209
+ throwValidateError(name, 'expects array.');
210
+ }
211
+ value.forEach((item, index) => {
212
+ validateParam(String(index), item, rule.items, name);
213
+ });
214
+ }
215
+
216
+ function validateShapeRule<TInput extends LooseRecord = LooseRecord>(
217
+ name: string,
218
+ value: unknown,
219
+ rule: ParamRule<TInput>
220
+ ): void {
221
+ if (!rule.shape || isNilValue(value)) return;
222
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
223
+ throwValidateError(name, 'expects object.');
224
+ }
225
+ const source = value as LooseRecord;
226
+ for (const [key, childRule] of Object.entries(rule.shape)) {
227
+ validateParam(key, source[key], childRule, name);
228
+ }
229
+ }
230
+
231
+ function matchesValueType(value: unknown, type: string): boolean {
232
+ if (type === 'renderable') return isRenderableValue(value);
233
+ if (type === 'Node') return isDomNodeValue(value);
234
+ if (type === 'Element') return isDomElementValue(value);
235
+ if (type === 'HTMLElement') return isHtmlElementValue(value);
236
+ if (type === 'plainObject') return isPlainObject(value);
237
+ return getType(value) === type;
238
+ }
239
+
240
+ function getActualType(value: unknown): string {
241
+ return isPlainObject(value) ? 'object' : getType(value);
242
+ }
243
+
244
+ function runValidateConditions(
245
+ name: string,
246
+ value: unknown,
247
+ conditions: ValidateCondition | readonly ValidateCondition[] = []
248
+ ): void {
249
+ const list = Array.isArray(conditions)
250
+ ? conditions
251
+ : conditions
252
+ ? [conditions]
253
+ : [];
254
+ for (const c of list) {
255
+ let testFn: (val: unknown) => boolean;
256
+ let message: string;
257
+
258
+ if (typeof c === 'function') {
259
+ testFn = c;
260
+ message = 'does not satisfy the required condition.';
261
+ } else if (c && typeof c.test === 'function') {
262
+ testFn = c.test;
263
+ message = c.message || 'condition failed.';
264
+ } else {
265
+ throw new Error(
266
+ 'Validator: Condition must be a function or { test, message }.'
267
+ );
268
+ }
269
+
270
+ if (!testFn(value)) {
271
+ throwValidateError(name, message);
272
+ }
273
+ }
274
+ }
275
+
276
+ function formatValidateName(namespace: string, name: string): string {
277
+ return namespace ? `${namespace}.${name}` : name;
278
+ }
279
+
280
+ export function validateParam<TInput extends LooseRecord = LooseRecord>(
281
+ name: string,
282
+ value: unknown,
283
+ rule: ParamRuleInput<TInput> = {},
284
+ namespace = ''
285
+ ): unknown {
286
+ const config = normalizeRule(rule);
287
+ const label = formatValidateName(namespace, name);
288
+ const expectedTypes = Object.hasOwn(config, 'types')
289
+ ? config.types
290
+ : config.type;
291
+
292
+ if (config.required && isNilValue(value)) {
293
+ throwValidateError(label, 'is required.');
294
+ }
295
+
296
+ if (expectedTypes !== undefined) {
297
+ const types = Array.isArray(expectedTypes)
298
+ ? expectedTypes
299
+ : [expectedTypes];
300
+ const actualType = getActualType(value);
301
+ const typeMatch = types.some((t) => matchesValueType(value, t));
302
+ if (!typeMatch) {
303
+ const allowed = types.join(', ');
304
+ throwValidateError(label, `expects ${allowed}, but got ${actualType}.`);
305
+ }
306
+ }
307
+
308
+ if (Array.isArray(config.enum) && !config.enum.includes(value)) {
309
+ throwValidateError(
310
+ label,
311
+ `expects one of ${config.enum.map(stringifyExpected).join(', ')}.`
312
+ );
313
+ }
314
+
315
+ validateLengthRule(label, value, config);
316
+ validateNumberRule(label, value, config);
317
+ validatePlainRule(label, value, config);
318
+ validateItemsRule(label, value, config);
319
+ validateShapeRule(label, value, config);
320
+ runValidateConditions(label, value, config.conditions);
321
+
322
+ if (typeof config.validate === 'function') {
323
+ const valid = config.validate(value);
324
+ if (!valid) {
325
+ throwValidateError(
326
+ label,
327
+ config.message || 'does not satisfy the required condition.'
328
+ );
329
+ }
330
+ }
331
+
332
+ return value;
333
+ }
334
+
335
+ export function resolveProps<
336
+ TInput extends LooseRecord,
337
+ TSchema extends ResolveSchema<TInput>,
338
+ >(
339
+ input: TInput | null | undefined = {} as TInput,
340
+ schema: TSchema = {} as TSchema,
341
+ namespace = 'Options'
342
+ ): TInput & ResolvedProps<TSchema> {
343
+ const source = input == null ? {} : input;
344
+ if (typeof source !== 'object' || Array.isArray(source)) {
345
+ throw new Error(`${namespace} expects object.`);
346
+ }
347
+
348
+ const sourceRecord = source as TInput;
349
+ const resolved: LooseRecord = { ...sourceRecord };
350
+ const entries = Object.entries(schema || {}) as Array<
351
+ [string, ParamRuleInput<TInput>]
352
+ >;
353
+
354
+ for (const [key, rawRule] of entries) {
355
+ const rule = normalizeRule<TInput>(rawRule);
356
+ resolved[key] = Object.hasOwn(sourceRecord, key)
357
+ ? sourceRecord[key]
358
+ : resolveDefault(rule);
359
+ }
360
+
361
+ for (const [key, rawRule] of entries) {
362
+ const rule = normalizeRule<TInput>(rawRule);
363
+ if (typeof rule.normalize === 'function') {
364
+ resolved[key] = rule.normalize(resolved[key], {
365
+ key,
366
+ input: sourceRecord,
367
+ options: resolved,
368
+ schema,
369
+ });
370
+ }
371
+ }
372
+
373
+ for (const [key, rule] of entries) {
374
+ validateParam(key, resolved[key], rule, namespace);
375
+ }
376
+
377
+ return resolved as TInput & ResolvedProps<TSchema>;
378
+ }
379
+
380
+ interface DefaultRule {
381
+ default?: unknown;
382
+ factory?: boolean;
383
+ }
384
+
385
+ function resolveDefault(rule: DefaultRule): unknown {
386
+ if (!Object.hasOwn(rule, 'default')) return undefined;
387
+ const defaultValue = rule.default;
388
+ const value =
389
+ rule.factory && typeof defaultValue === 'function'
390
+ ? defaultValue()
391
+ : defaultValue;
392
+ return cloneDefault(value);
393
+ }
394
+
395
+ /**
396
+ * 判断是否为普通可构造函数(排除箭头函数和类)。
397
+ * @param fn - 需要判断的值
398
+ * @returns 如果是普通函数则返回 true,否则返回 false
399
+ * @deprecated 未使用过,暂时注释掉。待需要使用时,再重新恢复。
400
+ */
401
+ // export const isFunction = (fn: unknown): fn is (...args: never[]) => unknown => {
402
+ // return (
403
+ // typeof fn === "function" &&
404
+ // Object.hasOwn(fn, "prototype") &&
405
+ // fn.prototype !== null &&
406
+ // typeof fn.prototype === "object" &&
407
+ // Object.hasOwn(fn.prototype, "constructor") &&
408
+ // fn.prototype.constructor === fn
409
+ // );
410
+ // };
411
+
412
+ /**
413
+ * 判断是否为类(使用 class 语法定义)。
414
+ * @param fn - 需要判断的值
415
+ * @returns 如果是类则返回 true,否则返回 false
416
+ * @deprecated 未使用过,暂时注释掉。待需要使用时,再重新恢复。
417
+ */
418
+ // export const isClass = (fn: unknown): fn is abstract new (...args: never[]) => unknown => {
419
+ // return typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
420
+ // };
@@ -1,29 +1,25 @@
1
1
  import { jsx } from 'vanilla-signal';
2
2
 
3
+ import { type DOMReference, all, resolveElement } from '../utilities/dom.ts';
4
+ import { type IEventManager, createEventManager } from '../utilities/events.ts';
3
5
  import {
4
6
  type ResolveSchema,
5
7
  resolveProps,
6
8
  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';
9
+ } from '../utilities/types.ts';
10
10
 
11
11
  type ValidatorElement =
12
12
  | HTMLInputElement
13
13
  | HTMLSelectElement
14
14
  | HTMLTextAreaElement;
15
- type ValidatorRuleName = keyof ValidatorRule | string;
16
- type ValidatorMessageMap = Record<
17
- string,
18
- Partial<Record<ValidatorRuleName, string>>
19
- >;
15
+ type ValidatorMessageMap = Record<string, Partial<Record<string, string>>>;
20
16
  type ValidatorCustomResult = boolean | string;
21
17
  type ValidatorCustomRule = (
22
18
  element: ValidatorElement,
23
- validator: Validator
19
+ validator: ValidatorInstance
24
20
  ) => ValidatorCustomResult;
25
21
 
26
- export interface ValidatorRule extends Record<string, unknown> {
22
+ interface ValidatorRule extends Record<string, unknown> {
27
23
  required?: boolean;
28
24
  minLength?: number;
29
25
  maxLength?: number;
@@ -45,16 +41,20 @@ export interface ValidatorRule extends Record<string, unknown> {
45
41
  validate?: ValidatorCustomRule;
46
42
  }
47
43
 
48
- export interface ValidatorOptions extends Record<string, unknown> {
44
+ interface ValidatorProps extends Record<string, unknown> {
49
45
  rules?: Record<string, ValidatorRule>;
50
46
  messages?: ValidatorMessageMap;
51
- onSubmit?: ((validator: Validator) => void) | null;
47
+ onSubmit?: ((validator: ValidatorInstance) => void) | null;
52
48
  }
53
49
 
54
- interface ResolvedValidatorOptions extends Record<string, unknown> {
50
+ interface ResolvedValidatorProps extends Record<string, unknown> {
55
51
  rules: Record<string, ValidatorRule>;
56
52
  messages: ValidatorMessageMap;
57
- onSubmit: ((validator: Validator) => void) | null;
53
+ onSubmit: ((validator: ValidatorInstance) => void) | null;
54
+ }
55
+
56
+ interface ValidatorDOM {
57
+ root: HTMLFormElement | null;
58
58
  }
59
59
 
60
60
  interface ValidatorRuntime {
@@ -71,22 +71,27 @@ interface ResetOptions {
71
71
  native?: boolean;
72
72
  }
73
73
 
74
- const VALIDATOR_OPTIONS_SCHEMA = {
74
+ interface ValidatorInstance {
75
+ dom: ValidatorDOM;
76
+ props: ResolvedValidatorProps | null;
77
+ runtime: ValidatorRuntime;
78
+ validate(): boolean;
79
+ reset(options?: ResetOptions): void;
80
+ destroy(): void;
81
+ }
82
+
83
+ const VALIDATOR_PROPS_SCHEMA = {
75
84
  rules: { default: {}, type: 'object' },
76
85
  messages: { default: {}, type: 'object' },
77
86
  onSubmit: { default: null, types: ['function', 'null'] },
78
- } satisfies ResolveSchema<ValidatorOptions>;
87
+ } satisfies ResolveSchema<ValidatorProps>;
79
88
 
80
- function normalizeOptions(input: ValidatorOptions): ResolvedValidatorOptions {
81
- const options = resolveProps(
82
- input,
83
- VALIDATOR_OPTIONS_SCHEMA,
84
- 'Validator.options'
85
- );
89
+ function normalizeProps(input: ValidatorProps): ResolvedValidatorProps {
90
+ const props = resolveProps(input, VALIDATOR_PROPS_SCHEMA, 'Validator.props');
86
91
  return {
87
- rules: options.rules as Record<string, ValidatorRule>,
88
- messages: options.messages as ValidatorMessageMap,
89
- onSubmit: options.onSubmit as ResolvedValidatorOptions['onSubmit'],
92
+ rules: props.rules as Record<string, ValidatorRule>,
93
+ messages: props.messages as ValidatorMessageMap,
94
+ onSubmit: props.onSubmit as ResolvedValidatorProps['onSubmit'],
90
95
  };
91
96
  }
92
97
 
@@ -146,19 +151,21 @@ function hasFiles(element: HTMLInputElement): element is HTMLInputElement & {
146
151
  *
147
152
  * 支持绑定表单 submit/reset 事件,也可以手动调用 validate/reset。
148
153
  */
149
- export class Validator {
150
- root: HTMLFormElement | null;
151
- options: ResolvedValidatorOptions | null;
154
+ class Validator implements ValidatorInstance {
155
+ dom: ValidatorDOM;
156
+ props: ResolvedValidatorProps | null;
152
157
  runtime: ValidatorRuntime;
153
158
  cleanup: ValidatorCleanup | null;
154
159
 
155
160
  constructor(
156
161
  element: DOMReference,
157
- options: ValidatorOptions = {},
162
+ props: ValidatorProps = {},
158
163
  bindEvents = false
159
164
  ) {
160
- this.options = normalizeOptions(options);
161
- this.root = this.resolveRoot(element);
165
+ this.props = normalizeProps(props);
166
+ this.dom = {
167
+ root: this.resolveRoot(element),
168
+ };
162
169
  this.runtime = createRuntime();
163
170
  this.cleanup = {
164
171
  events: createEventManager(),
@@ -185,13 +192,14 @@ export class Validator {
185
192
 
186
193
  private bindEvents(): void {
187
194
  this.unbindEvents();
188
- if (!this.root || !this.cleanup) return;
195
+ const { root } = this.dom;
196
+ if (!root || !this.cleanup) return;
189
197
 
190
- this.cleanup.events.on('submit', this.root, 'submit', (event) => {
198
+ this.cleanup.events.on('submit', root, 'submit', (event) => {
191
199
  event.preventDefault();
192
200
  this.validate();
193
201
  });
194
- this.cleanup.events.on('reset', this.root, 'reset', () => {
202
+ this.cleanup.events.on('reset', root, 'reset', () => {
195
203
  this.reset({ native: false });
196
204
  });
197
205
  }
@@ -204,28 +212,29 @@ export class Validator {
204
212
  * 执行表单校验。
205
213
  */
206
214
  validate(): boolean {
207
- if (!this.root || !this.options) return false;
215
+ const { root } = this.dom;
216
+ if (!root || !this.props) return false;
208
217
 
209
218
  this.runtime.valid = true;
210
219
  this.runtime.message = '';
211
220
 
212
- for (const element of Array.from(this.root.elements)) {
221
+ for (const element of Array.from(root.elements)) {
213
222
  if (!isValidatorElement(element) || !element.name) continue;
214
- if (!this.options.rules[element.name]) continue;
223
+ if (!this.props.rules[element.name]) continue;
215
224
 
216
225
  this.runtime.valid = this.validateRule(element, element.name);
217
226
  if (!this.runtime.valid) break;
218
227
  }
219
228
 
220
- if (this.runtime.valid && this.options.onSubmit) {
221
- this.options.onSubmit(this);
229
+ if (this.runtime.valid && this.props.onSubmit) {
230
+ this.props.onSubmit(this);
222
231
  }
223
232
  return this.runtime.valid;
224
233
  }
225
234
 
226
235
  private validateRule(element: ValidatorElement, name: string): boolean {
227
- if (!this.options) return false;
228
- const rules = this.options.rules[name];
236
+ if (!this.props) return false;
237
+ const rules = this.props.rules[name];
229
238
  if (!rules) return true;
230
239
 
231
240
  if (this.hasNativeValidationRule(element)) {
@@ -358,9 +367,10 @@ export class Validator {
358
367
  element: ValidatorElement,
359
368
  targetName: unknown
360
369
  ): boolean {
361
- if (!this.root || typeof targetName !== 'string') return true;
370
+ const { root } = this.dom;
371
+ if (!root || typeof targetName !== 'string') return true;
362
372
  const targetElement = toValidatorElement(
363
- this.root.elements.namedItem(targetName)
373
+ root.elements.namedItem(targetName)
364
374
  );
365
375
  if (!targetElement) {
366
376
  throw new Error(`Validator: target element "${targetName}" not found.`);
@@ -509,7 +519,7 @@ export class Validator {
509
519
  element.classList.add('is-invalid');
510
520
  }
511
521
 
512
- const error = customMessage || this.options?.messages[name]?.[rule] || '';
522
+ const error = customMessage || this.props?.messages[name]?.[rule] || '';
513
523
  if (!error) return;
514
524
 
515
525
  this.runtime.message = error;
@@ -552,16 +562,17 @@ export class Validator {
552
562
  * 重置表单与校验状态。
553
563
  */
554
564
  reset({ native = true }: ResetOptions = {}): void {
555
- if (!this.root) return;
556
- if (native) this.root.reset();
565
+ const { root } = this.dom;
566
+ if (!root) return;
567
+ if (native) root.reset();
557
568
 
558
- for (const element of Array.from(this.root.elements)) {
569
+ for (const element of Array.from(root.elements)) {
559
570
  if (!isValidatorElement(element)) continue;
560
571
  element.classList.remove('is-valid');
561
572
  element.classList.remove('is-invalid');
562
573
  }
563
574
 
564
- for (const help of all<HTMLElement>('[data-validator-help]', this.root)) {
575
+ for (const help of all<HTMLElement>('[data-validator-help]', root)) {
565
576
  help.remove();
566
577
  }
567
578
 
@@ -578,8 +589,8 @@ export class Validator {
578
589
 
579
590
  this.unbindEvents();
580
591
  this.reset({ native: false });
581
- this.root = null;
582
- this.options = null;
592
+ this.dom.root = null;
593
+ this.props = null;
583
594
  this.runtime.valid = false;
584
595
  this.runtime.message = '';
585
596
  this.cleanup?.events.clear();
@@ -589,8 +600,8 @@ export class Validator {
589
600
 
590
601
  export function createValidator(
591
602
  element: DOMReference,
592
- options: ValidatorOptions = {},
603
+ props: ValidatorProps = {},
593
604
  bindEvents = false
594
- ): Validator {
595
- return new Validator(element, options, bindEvents);
605
+ ): ValidatorInstance {
606
+ return new Validator(element, props, bindEvents);
596
607
  }