vanilla-jui 1.4.5 → 1.4.6
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/README.md +6 -0
- package/dist/index.d.ts +1273 -1896
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +21 -2
- package/src/components/accordion.ts +297 -216
- package/src/components/flow.ts +222 -258
- package/src/components/form.ts +44 -102
- package/src/components/menu.ts +268 -356
- package/src/components/modal.ts +181 -641
- package/src/components/offcanvas.ts +225 -106
- package/src/components/pagination.ts +296 -326
- package/src/components/sticky.ts +55 -54
- package/src/components/swiper.ts +237 -247
- package/src/components/tabs.ts +278 -271
- package/src/components/toc.ts +42 -46
- package/src/core/Component.ts +69 -47
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +1 -5
- package/src/css/avatar.css +1 -1
- package/src/css/badge.css +1 -1
- package/src/css/breadcrumb.css +9 -10
- package/src/css/button.css +1 -1
- package/src/css/card.css +2 -2
- package/src/css/color.css +974 -1002
- package/src/css/color_p3.css +974 -1004
- package/src/css/components.css +1 -1
- package/src/css/content.css +72 -63
- package/src/css/drop.css +1 -1
- package/src/css/editor.css +14 -14
- package/src/css/flow.css +4 -14
- package/src/css/font.css +5 -4
- package/src/css/form.css +76 -52
- package/src/css/gradient.css +299 -222
- package/src/css/icon.css +57 -57
- package/src/css/index.css +1 -1
- package/src/css/layout.css +13 -10
- package/src/css/loading.css +60 -12
- package/src/css/menu.css +262 -188
- package/src/css/modal.css +1 -1
- package/src/css/offcanvas.css +9 -13
- package/src/css/pagination.css +5 -1
- package/src/css/popup.css +1 -1
- package/src/css/skeleton.css +3 -2
- package/src/css/status.css +2 -3
- package/src/css/swiper.css +1 -1
- package/src/css/table.css +1 -1
- package/src/css/tabs.css +12 -11
- package/src/css/tag.css +1 -1
- package/src/css/theme-palette.css +19 -5
- package/src/css/themes/default/accordion.css +61 -61
- package/src/css/themes/default/avatar.css +54 -54
- package/src/css/themes/default/badge.css +62 -62
- package/src/css/themes/default/breadcrumb.css +23 -23
- package/src/css/themes/default/button.css +203 -203
- package/src/css/themes/default/content.css +53 -53
- package/src/css/themes/default/drop.css +10 -10
- package/src/css/themes/default/editor.css +9 -9
- package/src/css/themes/default/index.css +1 -1
- package/src/css/themes/default/loading.css +26 -20
- package/src/css/themes/default/menu.css +75 -11
- package/src/css/themes/default/modal.css +32 -36
- package/src/css/themes/default/pagination.css +7 -7
- package/src/css/themes/default/popup.css +6 -6
- package/src/css/themes/default/swiper.css +32 -32
- package/src/css/themes/default/table.css +16 -16
- package/src/css/themes/default/tabs.css +30 -30
- package/src/css/themes/default/tag.css +71 -71
- package/src/css/themes/default/theme-palette.css +11 -11
- package/src/css/themes/default/tip.css +42 -42
- package/src/css/themes/default/toast.css +60 -60
- package/src/css/themes/default/toolbar.css +23 -29
- package/src/css/themes/default/tooltip.css +43 -13
- package/src/css/themes/index.css +1 -1
- package/src/css/tip.css +2 -2
- package/src/css/toast.css +3 -3
- package/src/css/tokens.css +12 -17
- package/src/css/toolbar.css +9 -10
- package/src/css/tooltip.css +3 -2
- package/src/index.ts +22 -19
- package/src/{components → primitives}/drop.ts +58 -49
- package/src/{components → primitives}/icons.ts +1 -1
- package/src/primitives/loading.ts +23 -0
- package/src/primitives/parabola.ts +388 -0
- package/src/primitives/popup.ts +61 -0
- package/src/{components → primitives}/theme.ts +88 -57
- package/src/{components → primitives}/toast.ts +18 -25
- package/src/{components → primitives}/tooltip.ts +100 -25
- package/src/utilities/browser.ts +23 -49
- package/src/utilities/class-name.ts +5 -0
- package/src/utilities/dom.ts +14 -32
- package/src/utilities/function.ts +4 -0
- package/src/utilities/http.ts +7 -7
- package/src/utilities/id.ts +57 -0
- package/src/utilities/object.ts +5 -0
- package/src/utilities/timer.ts +38 -0
- package/src/utilities/types.ts +420 -0
- package/src/{components → validation}/validator.ts +64 -53
- package/src/components/parabola.ts +0 -335
- package/src/utilities/cache.ts +0 -57
- package/src/utilities/core.ts +0 -614
- 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/
|
|
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
|
|
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:
|
|
19
|
+
validator: ValidatorInstance
|
|
24
20
|
) => ValidatorCustomResult;
|
|
25
21
|
|
|
26
|
-
|
|
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
|
-
|
|
44
|
+
interface ValidatorProps extends Record<string, unknown> {
|
|
49
45
|
rules?: Record<string, ValidatorRule>;
|
|
50
46
|
messages?: ValidatorMessageMap;
|
|
51
|
-
onSubmit?: ((validator:
|
|
47
|
+
onSubmit?: ((validator: ValidatorInstance) => void) | null;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
interface
|
|
50
|
+
interface ResolvedValidatorProps extends Record<string, unknown> {
|
|
55
51
|
rules: Record<string, ValidatorRule>;
|
|
56
52
|
messages: ValidatorMessageMap;
|
|
57
|
-
onSubmit: ((validator:
|
|
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
|
-
|
|
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<
|
|
87
|
+
} satisfies ResolveSchema<ValidatorProps>;
|
|
79
88
|
|
|
80
|
-
function
|
|
81
|
-
const
|
|
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:
|
|
88
|
-
messages:
|
|
89
|
-
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
162
|
+
props: ValidatorProps = {},
|
|
158
163
|
bindEvents = false
|
|
159
164
|
) {
|
|
160
|
-
this.
|
|
161
|
-
this.
|
|
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
|
-
|
|
195
|
+
const { root } = this.dom;
|
|
196
|
+
if (!root || !this.cleanup) return;
|
|
189
197
|
|
|
190
|
-
this.cleanup.events.on('submit',
|
|
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',
|
|
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
|
-
|
|
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(
|
|
221
|
+
for (const element of Array.from(root.elements)) {
|
|
213
222
|
if (!isValidatorElement(element) || !element.name) continue;
|
|
214
|
-
if (!this.
|
|
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.
|
|
221
|
-
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.
|
|
228
|
-
const rules = this.
|
|
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
|
-
|
|
370
|
+
const { root } = this.dom;
|
|
371
|
+
if (!root || typeof targetName !== 'string') return true;
|
|
362
372
|
const targetElement = toValidatorElement(
|
|
363
|
-
|
|
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.
|
|
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
|
-
|
|
556
|
-
if (
|
|
565
|
+
const { root } = this.dom;
|
|
566
|
+
if (!root) return;
|
|
567
|
+
if (native) root.reset();
|
|
557
568
|
|
|
558
|
-
for (const element of Array.from(
|
|
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]',
|
|
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.
|
|
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
|
-
|
|
603
|
+
props: ValidatorProps = {},
|
|
593
604
|
bindEvents = false
|
|
594
|
-
):
|
|
595
|
-
return new Validator(element,
|
|
605
|
+
): ValidatorInstance {
|
|
606
|
+
return new Validator(element, props, bindEvents);
|
|
596
607
|
}
|