vanilla-jui 1.3.11 → 1.4.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/dist/index.d.ts +2921 -0
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -3
- package/src/components/accordion.ts +522 -0
- package/src/components/drop.ts +497 -0
- package/src/components/flow.ts +1579 -0
- package/src/components/form.ts +953 -0
- package/src/components/{icons.js → icons.ts} +24 -11
- package/src/components/menu.ts +615 -0
- package/src/components/{modal.js → modal.ts} +560 -237
- package/src/components/offcanvas.ts +457 -0
- package/src/components/{pagination.js → pagination.ts} +251 -76
- package/src/components/parabola.ts +335 -0
- package/src/components/{sticky.js → sticky.ts} +123 -98
- package/src/components/{swiper.js → swiper.ts} +510 -232
- package/src/components/tabs.ts +984 -0
- package/src/components/theme.ts +437 -0
- package/src/components/toast.ts +371 -0
- package/src/components/toc.ts +385 -0
- package/src/components/tooltip.ts +179 -0
- package/src/components/validator.ts +596 -0
- package/src/core/{Component.js → Component.ts} +134 -27
- package/src/css/accordion.css +0 -45
- package/src/css/animation.css +31 -1
- package/src/css/avatar.css +7 -33
- package/src/css/badge.css +11 -59
- package/src/css/breadcrumb.css +9 -32
- package/src/css/button.css +23 -224
- package/src/css/card.css +1 -1
- package/src/css/color.css +863 -1013
- package/src/css/color_p3.css +822 -975
- package/src/css/components.css +1 -0
- package/src/css/drop.css +7 -7
- package/src/css/flow.css +1 -1
- package/src/css/form.css +1 -1
- package/src/css/icon.css +7 -12
- package/src/css/index.css +5 -2
- package/src/css/loading.css +36 -20
- package/src/css/menu.css +1 -1
- package/src/css/modal.css +15 -44
- package/src/css/offcanvas.css +14 -40
- package/src/css/popup.css +4 -4
- package/src/css/status.css +0 -2
- package/src/css/swiper.css +35 -50
- package/src/css/tabs.css +9 -31
- package/src/css/tag.css +16 -77
- package/src/css/themes/default/accordion.css +62 -0
- package/src/css/themes/default/avatar.css +39 -0
- package/src/css/themes/default/badge.css +63 -0
- package/src/css/themes/default/breadcrumb.css +28 -0
- package/src/css/themes/default/button.css +204 -0
- package/src/css/themes/default/drop.css +11 -0
- package/src/css/themes/default/index.css +16 -0
- package/src/css/themes/default/loading.css +23 -0
- package/src/css/themes/default/modal.css +40 -0
- package/src/css/themes/default/offcanvas.css +13 -0
- package/src/css/themes/default/popup.css +7 -0
- package/src/css/themes/default/swiper.css +35 -0
- package/src/css/themes/default/tabs.css +37 -0
- package/src/css/themes/default/tag.css +85 -0
- package/src/css/themes/default/tip.css +49 -0
- package/src/css/themes/default/toast.css +70 -0
- package/src/css/themes/default/tooltip.css +14 -0
- package/src/css/themes/index.css +1 -0
- package/src/css/tip.css +20 -39
- package/src/css/toast.css +18 -80
- package/src/css/tokens.css +8 -3
- package/src/css/tooltip.css +10 -10
- package/src/index.ts +37 -0
- package/src/locales/{index.js → index.ts} +1 -2
- package/src/types.d.ts +1 -0
- package/src/utilities/browser.ts +131 -0
- package/src/utilities/cache.ts +57 -0
- package/src/utilities/core.ts +614 -0
- package/src/utilities/dom.ts +468 -0
- package/src/utilities/events.ts +106 -0
- package/src/utilities/http.ts +35 -0
- package/src/utilities/storage.ts +46 -0
- package/src/components/accordion.js +0 -373
- package/src/components/components.js +0 -0
- package/src/components/drop.js +0 -437
- package/src/components/flow.js +0 -1133
- package/src/components/form.js +0 -585
- package/src/components/menu.js +0 -422
- package/src/components/offcanvas.js +0 -332
- package/src/components/parabola.js +0 -295
- package/src/components/tabs.js +0 -746
- package/src/components/theme.js +0 -353
- package/src/components/toast.js +0 -329
- package/src/components/toc.js +0 -226
- package/src/components/tooltip.js +0 -86
- package/src/components/validator.js +0 -586
- package/src/index.js +0 -45
- package/src/locales/index.min.js +0 -1
- package/src/utilities/browser.js +0 -111
- package/src/utilities/cache.js +0 -159
- package/src/utilities/core.js +0 -502
- package/src/utilities/debug.js +0 -103
- package/src/utilities/dom.js +0 -313
- package/src/utilities/events.js +0 -69
- package/src/utilities/http.js +0 -30
- package/src/utilities/legacy-dom.js +0 -179
- package/src/utilities/storage.js +0 -46
- /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
|
+
}
|