vanilla-jui 1.3.10 → 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 (90) 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/accordion.css +2 -2
  26. package/src/css/animation.css +31 -1
  27. package/src/css/breadcrumb.css +1 -1
  28. package/src/css/button.css +5 -5
  29. package/src/css/card.css +7 -7
  30. package/src/css/color.css +865 -1028
  31. package/src/css/color_p3.css +871 -1036
  32. package/src/css/components.css +1 -0
  33. package/src/css/drop.css +1 -1
  34. package/src/css/flow.css +1 -1
  35. package/src/css/form.css +21 -21
  36. package/src/css/gradient.css +17 -4
  37. package/src/css/icon.css +17 -1
  38. package/src/css/index.css +3 -3
  39. package/src/css/layout.css +1 -1
  40. package/src/css/menu.css +27 -27
  41. package/src/css/modal.css +10 -27
  42. package/src/css/offcanvas.css +2 -2
  43. package/src/css/pagination.css +1 -1
  44. package/src/css/popup.css +2 -2
  45. package/src/css/swiper.css +12 -12
  46. package/src/css/tabs.css +3 -2
  47. package/src/css/tag.css +3 -4
  48. package/src/css/theme-palette.css +6 -6
  49. package/src/css/tip.css +1 -1
  50. package/src/css/toast.css +8 -8
  51. package/src/css/tokens.css +197 -226
  52. package/src/css/toolbar.css +3 -3
  53. package/src/css/tooltip.css +1 -1
  54. package/src/index.ts +37 -0
  55. package/src/locales/{index.js → index.ts} +1 -2
  56. package/src/types.d.ts +1 -0
  57. package/src/utilities/browser.ts +131 -0
  58. package/src/utilities/cache.ts +57 -0
  59. package/src/utilities/core.ts +614 -0
  60. package/src/utilities/dom.ts +468 -0
  61. package/src/utilities/events.ts +106 -0
  62. package/src/utilities/http.ts +35 -0
  63. package/src/utilities/storage.ts +46 -0
  64. package/src/components/accordion.js +0 -373
  65. package/src/components/drop.js +0 -437
  66. package/src/components/flow.js +0 -1133
  67. package/src/components/form.js +0 -585
  68. package/src/components/menu.js +0 -422
  69. package/src/components/offcanvas.js +0 -332
  70. package/src/components/parabola.js +0 -295
  71. package/src/components/tabs.js +0 -746
  72. package/src/components/theme.js +0 -353
  73. package/src/components/toast.js +0 -329
  74. package/src/components/toc.js +0 -226
  75. package/src/components/tooltip.js +0 -86
  76. package/src/components/validator.js +0 -586
  77. package/src/css/toast.min.css +0 -1
  78. package/src/index.js +0 -45
  79. package/src/locales/index.min.js +0 -1
  80. package/src/utilities/browser.js +0 -111
  81. package/src/utilities/cache.js +0 -159
  82. package/src/utilities/core.js +0 -502
  83. package/src/utilities/debug.js +0 -103
  84. package/src/utilities/dom.js +0 -313
  85. package/src/utilities/events.js +0 -69
  86. package/src/utilities/http.js +0 -30
  87. package/src/utilities/legacy-dom.js +0 -179
  88. package/src/utilities/storage.js +0 -46
  89. /package/src/{components/components.js → css/themes/index.css} +0 -0
  90. /package/src/icons/{index.js → index.ts} +0 -0
@@ -0,0 +1,614 @@
1
+ type LooseRecord = Record<string, unknown>;
2
+ type ValueTypeName = string;
3
+ type TypeRule = ValueTypeName | readonly ValueTypeName[];
4
+ type AnyFunction = (this: unknown, ...args: never[]) => unknown;
5
+
6
+ export type ValidateCondition =
7
+ | ((value: unknown) => boolean)
8
+ | {
9
+ test: (value: unknown) => boolean;
10
+ message?: string;
11
+ };
12
+
13
+ export interface NormalizeContext<TInput extends LooseRecord = LooseRecord> {
14
+ key: string;
15
+ input: TInput;
16
+ options: LooseRecord;
17
+ schema: ResolveSchema<TInput>;
18
+ }
19
+
20
+ export interface ParamRule<TInput extends LooseRecord = LooseRecord> {
21
+ type?: TypeRule;
22
+ types?: TypeRule;
23
+ required?: boolean;
24
+ enum?: readonly unknown[];
25
+ conditions?: ValidateCondition | readonly ValidateCondition[];
26
+ validate?: (value: unknown) => boolean;
27
+ message?: string;
28
+ normalize?: (value: unknown, context: NormalizeContext<TInput>) => unknown;
29
+ default?: unknown;
30
+ factory?: boolean;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ export type ParamRuleInput<TInput extends LooseRecord = LooseRecord> =
35
+ | TypeRule
36
+ | ParamRule<TInput>;
37
+
38
+ export type ResolveSchema<TInput extends LooseRecord = LooseRecord> = Record<
39
+ string,
40
+ ParamRuleInput<TInput>
41
+ >;
42
+
43
+ export type ResolvedProps<TSchema extends object> = LooseRecord & {
44
+ [Key in keyof TSchema]: unknown;
45
+ };
46
+
47
+ /**
48
+ * 判断对象是否包含指定的自有属性。
49
+ * @template T - 对象类型
50
+ * @param obj - 待检查的对象
51
+ * @param key - 属性名
52
+ * @returns 如果对象包含指定自有属性则返回 true,否则返回 false
53
+ */
54
+ export const hasOwn = <T extends object>(
55
+ obj: T,
56
+ key: PropertyKey
57
+ ): key is keyof T => Object.prototype.hasOwnProperty.call(obj, key);
58
+
59
+ /**
60
+ * 创建去重后的数组,并移除假值(null、undefined、false、0、NaN、空字符串)。
61
+ * @template T - 数组元素类型
62
+ * @param list - 待去重的数组
63
+ * @returns 去重后的新数组
64
+ */
65
+ export const uniq = <T>(list: readonly T[]): T[] =>
66
+ Array.from(new Set(list.filter(Boolean)));
67
+
68
+ /**
69
+ * 定时器管理器
70
+ */
71
+ export const timer = {
72
+ // 存储定时器 { key: timerId }
73
+ timers: {} as Record<string, ReturnType<typeof setTimeout>>,
74
+
75
+ /**
76
+ * 注册并开始一个定时器
77
+ * @param {string} key - 定时器的唯一标识
78
+ * @param {number} duration - 延迟执行的时间(毫秒)
79
+ * @param {function} callback - 延迟执行的回调函数
80
+ */
81
+ start(key: string, duration: number, callback: () => void) {
82
+ // 1. 如果该 key 已经存在,先清除旧的定时器,防止重复触发
83
+ if (this.timers[key]) {
84
+ clearTimeout(this.timers[key]);
85
+ }
86
+ // 2. 设置新的定时器,并保存其 ID
87
+ this.timers[key] = setTimeout(() => {
88
+ callback();
89
+ // 3. 执行完毕后,自动从管理器中注销该定时器
90
+ delete this.timers[key];
91
+ }, duration);
92
+ },
93
+
94
+ /**
95
+ * 明确注销一个定时器
96
+ * @param {string} key - 需要取消的定时器唯一标识
97
+ */
98
+ cancel(key: string) {
99
+ if (this.timers[key]) {
100
+ clearTimeout(this.timers[key]);
101
+ // 从管理器中移除该记录
102
+ delete this.timers[key];
103
+ }
104
+ },
105
+ };
106
+
107
+ /**
108
+ * 获取值的增强类型名称。
109
+ * 支持识别 null、array、HTMLElement、Node 等特殊类型。
110
+ * @param val - 需要判断类型的值
111
+ * @returns 类型名称(如 array、null、HTMLElement、Node、string)
112
+ */
113
+ export const getType = (val: unknown): string => {
114
+ if (val === null) return 'null';
115
+ if (Array.isArray(val)) return 'array';
116
+ if (typeof HTMLElement !== 'undefined' && val instanceof HTMLElement) {
117
+ return 'HTMLElement';
118
+ }
119
+ if (typeof Node !== 'undefined' && val instanceof Node) return 'Node';
120
+ return typeof val;
121
+ };
122
+
123
+ /**
124
+ * 判断是否为普通可构造函数(排除箭头函数和类)。
125
+ * @param fn - 需要判断的值
126
+ * @returns 如果是普通函数则返回 true,否则返回 false
127
+ */
128
+ export const isFunction = (
129
+ fn: unknown
130
+ ): fn is (...args: never[]) => unknown => {
131
+ return (
132
+ typeof fn === 'function' &&
133
+ hasOwn(fn, 'prototype') &&
134
+ fn.prototype !== null &&
135
+ typeof fn.prototype === 'object' &&
136
+ hasOwn(fn.prototype, 'constructor') &&
137
+ fn.prototype.constructor === fn
138
+ );
139
+ };
140
+
141
+ /**
142
+ * 判断是否为类(使用 class 语法定义)。
143
+ * @param fn - 需要判断的值
144
+ * @returns 如果是类则返回 true,否则返回 false
145
+ */
146
+ export const isClass = (
147
+ fn: unknown
148
+ ): fn is abstract new (...args: never[]) => unknown => {
149
+ return (
150
+ typeof fn === 'function' &&
151
+ /^class\s/.test(Function.prototype.toString.call(fn))
152
+ );
153
+ };
154
+
155
+ /**
156
+ * 判断是否为普通对象(纯对象字面量或通过 Object.create(null) 创建)。
157
+ * @param value - 需要判断的值
158
+ * @returns 如果是普通对象则返回 true,否则返回 false
159
+ */
160
+ export function isPlainObject(
161
+ value: unknown
162
+ ): value is Record<string, unknown> {
163
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
164
+ const proto = Object.getPrototypeOf(value);
165
+ return proto === Object.prototype || proto === null;
166
+ }
167
+
168
+ /**
169
+ * 浅拷贝默认值。
170
+ * @param {*} value 需要拷贝的值。
171
+ * @returns {*} 拷贝后的值。
172
+ */
173
+ function cloneDefault<T>(value: T): T {
174
+ if (Array.isArray(value)) return value.slice() as T;
175
+ if (isPlainObject(value)) return { ...value } as T;
176
+ return value;
177
+ }
178
+
179
+ /**
180
+ * 规范化规则对象。
181
+ * @param {string|string[]|object} rule 需要规范化的规则对象。
182
+ * @returns {NormalizedRule} 规范化后的规则对象。
183
+ */
184
+ function normalizeRule<TInput extends LooseRecord = LooseRecord>(
185
+ rule: ParamRuleInput<TInput> = {}
186
+ ): ParamRule<TInput> {
187
+ if (typeof rule === 'string' || Array.isArray(rule)) return { type: rule };
188
+ if (!rule || typeof rule !== 'object') return {};
189
+ return rule as ParamRule<TInput>;
190
+ }
191
+
192
+ /**
193
+ * 运行验证条件。
194
+ * @param {string} name 参数名称,用于错误信息。
195
+ * @param {*} value 参数值。
196
+ * @param {ValidateConditionRule[]} [conditions=[]] 附加条件。
197
+ * @throws {Error} 条件失败时抛出。
198
+ */
199
+ function runValidateConditions(
200
+ name: string,
201
+ value: unknown,
202
+ conditions: ValidateCondition | readonly ValidateCondition[] = []
203
+ ): void {
204
+ const list = Array.isArray(conditions)
205
+ ? conditions
206
+ : conditions
207
+ ? [conditions]
208
+ : [];
209
+ for (const c of list) {
210
+ let testFn: (val: unknown) => boolean;
211
+ let message: string;
212
+
213
+ if (typeof c === 'function') {
214
+ testFn = c;
215
+ message = 'does not satisfy the required condition.';
216
+ } else if (c && typeof c.test === 'function') {
217
+ testFn = c.test;
218
+ message = c.message || 'condition failed.';
219
+ } else {
220
+ throw new Error(
221
+ 'Validator: Condition must be a function or { test, message }.'
222
+ );
223
+ }
224
+
225
+ if (!testFn(value)) {
226
+ throw new Error(`Validator: ${name} ${message}`);
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 格式化错误名称。
233
+ * @param {string} namespace 命名空间。
234
+ * @param {string} name 参数名。
235
+ * @returns {string} 格式化后的错误名称。
236
+ */
237
+ function formatValidateName(namespace: string, name: string): string {
238
+ return namespace ? `${namespace}.${name}` : name;
239
+ }
240
+
241
+ /**
242
+ * 校验单个参数。
243
+ *
244
+ * 支持的校验规则包括:type/types(类型)、required(必填)、enum(枚举)、
245
+ * conditions(条件)、validate(自定义校验函数)、message(错误消息)。
246
+ *
247
+ * @template TInput - 输入对象类型
248
+ * @param name - 参数名
249
+ * @param value - 参数值
250
+ * @param [rule] - 校验规则
251
+ * @param [namespace] - 错误命名空间,用于错误消息前缀
252
+ * @returns 校验通过后的原值
253
+ * @throws {Error} 校验失败时抛出错误
254
+ */
255
+ export function validateParam<TInput extends LooseRecord = LooseRecord>(
256
+ name: string,
257
+ value: unknown,
258
+ rule: ParamRuleInput<TInput> = {},
259
+ namespace = ''
260
+ ): unknown {
261
+ const config = normalizeRule(rule);
262
+ const label = formatValidateName(namespace, name);
263
+ const expectedTypes = hasOwn(config, 'types') ? config.types : config.type;
264
+
265
+ if (config.required && (value === undefined || value === null)) {
266
+ throw new Error(`Validator: ${label} is required.`);
267
+ }
268
+
269
+ if (expectedTypes !== undefined) {
270
+ const types = Array.isArray(expectedTypes)
271
+ ? expectedTypes
272
+ : [expectedTypes];
273
+ const actualType = getType(value);
274
+ const typeMatch = types.some((t) => t === actualType);
275
+ if (!typeMatch) {
276
+ const allowed = types.join(', ');
277
+ throw new Error(
278
+ `Validator: ${label} expects ${allowed}, but got ${actualType}.`
279
+ );
280
+ }
281
+ runValidateConditions(label, value, config.conditions);
282
+ } else {
283
+ runValidateConditions(label, value, config.conditions);
284
+ }
285
+
286
+ if (Array.isArray(config.enum) && !config.enum.includes(value)) {
287
+ throw new Error(
288
+ `Validator: ${label} expects one of ${config.enum.join(', ')}.`
289
+ );
290
+ }
291
+
292
+ if (typeof config.validate === 'function') {
293
+ const valid = config.validate(value);
294
+ if (!valid) {
295
+ throw new Error(
296
+ `Validator: ${label} ${config.message || 'does not satisfy the required condition.'}`
297
+ );
298
+ }
299
+ }
300
+
301
+ return value;
302
+ }
303
+
304
+ /**
305
+ * 合并默认值、执行 normalize 并校验配置对象。
306
+ *
307
+ * schema 的每一项可同时定义:default(默认值)、factory(是否为工厂函数)、
308
+ * normalize(标准化函数)和校验规则(type、required、enum 等)。
309
+ *
310
+ * @template TInput - 输入对象类型
311
+ * @template TSchema - 配置 schema 类型
312
+ * @param [input={}] - 用户传入的配置对象
313
+ * @param [schema={}] - 配置 schema 定义
314
+ * @param [namespace="Options"] - 错误命名空间,用于错误消息前缀
315
+ * @returns 合并并校验后的配置对象
316
+ */
317
+ export function resolveProps<
318
+ TInput extends LooseRecord,
319
+ TSchema extends ResolveSchema<TInput>,
320
+ >(
321
+ input: TInput | null | undefined = {} as TInput,
322
+ schema: TSchema = {} as TSchema,
323
+ namespace = 'Options'
324
+ ): TInput & ResolvedProps<TSchema> {
325
+ const source = input == null ? {} : input;
326
+ if (typeof source !== 'object' || Array.isArray(source)) {
327
+ throw new Error(`${namespace} expects object.`);
328
+ }
329
+
330
+ const sourceRecord = source as TInput;
331
+ const resolved: LooseRecord = { ...sourceRecord };
332
+ const entries = Object.entries(schema || {}) as Array<
333
+ [string, ParamRuleInput<TInput>]
334
+ >;
335
+
336
+ // 1. 处理默认值
337
+ for (const [key, rawRule] of entries) {
338
+ const rule = normalizeRule<TInput>(rawRule);
339
+ resolved[key] = hasOwn(source, key) ? source[key] : resolveDefault(rule);
340
+ }
341
+
342
+ // 2. 执行 normalize 钩子
343
+ for (const [key, rawRule] of entries) {
344
+ const rule = normalizeRule<TInput>(rawRule);
345
+ if (typeof rule.normalize === 'function') {
346
+ resolved[key] = rule.normalize(resolved[key], {
347
+ key,
348
+ input: sourceRecord,
349
+ options: resolved,
350
+ schema,
351
+ });
352
+ }
353
+ }
354
+
355
+ // 3. 校验
356
+ for (const [key, rule] of entries) {
357
+ validateParam(key, resolved[key], rule, namespace);
358
+ }
359
+
360
+ return resolved as TInput & ResolvedProps<TSchema>;
361
+ }
362
+
363
+ /**
364
+ * 生成标准 UUID v4 字符串。
365
+ *
366
+ * 优先使用浏览器原生的 crypto.randomUUID(),
367
+ * 不支持时使用 polyfill 实现。
368
+ *
369
+ * @returns UUID v4 字符串(如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
370
+ */
371
+ export function uuid(): string {
372
+ if (typeof crypto.randomUUID === 'function') {
373
+ return crypto.randomUUID();
374
+ }
375
+ const bytes = new Uint8Array(16);
376
+ if (typeof crypto.getRandomValues !== 'function') {
377
+ throw new Error('Your browser is too old to support secure login.');
378
+ }
379
+ crypto.getRandomValues(bytes);
380
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
381
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
382
+ const hex = Array.from(bytes)
383
+ .map((b) => b.toString(16).padStart(2, '0'))
384
+ .join('');
385
+ return [
386
+ hex.slice(0, 8),
387
+ hex.slice(8, 12),
388
+ hex.slice(12, 16),
389
+ hex.slice(16, 20),
390
+ hex.slice(20, 32),
391
+ ].join('-');
392
+ }
393
+
394
+ /**
395
+ * 生成适合 DOM id 的随机字符串。
396
+ *
397
+ * 使用安全的随机数生成器,生成的字符串适合作为 HTML 元素的 id 属性。
398
+ *
399
+ * @param [length=8] - 字符串长度,范围 1 到 87381
400
+ * @returns 随机字符串
401
+ * @throws {Error} 长度不在有效范围内时抛出错误
402
+ */
403
+ export function randomId(length: number = 8): string {
404
+ if (!Number.isInteger(length) || length < 1 || length > 87381) {
405
+ throw new Error('Length must be an integer between 1 and 87381');
406
+ }
407
+
408
+ const byteLength = Math.ceil((length * 3) / 4);
409
+ const bytes = new Uint8Array(byteLength);
410
+ crypto.getRandomValues(bytes);
411
+
412
+ let binary = '';
413
+ for (const byte of bytes) {
414
+ binary += String.fromCharCode(byte);
415
+ }
416
+ const base64 = btoa(binary);
417
+
418
+ return base64.substring(0, length).replace(/\+/g, '-').replace(/\//g, '_');
419
+ }
420
+
421
+ interface PendingCall<T extends AnyFunction> {
422
+ receiver: ThisParameterType<T>;
423
+ args: Parameters<T>;
424
+ }
425
+
426
+ export interface DebounceOptions {
427
+ leading?: boolean;
428
+ trailing?: boolean;
429
+ maxWait?: number;
430
+ }
431
+
432
+ export type DebouncedFunction<T extends AnyFunction> = {
433
+ (
434
+ this: ThisParameterType<T>,
435
+ ...args: Parameters<T>
436
+ ): ReturnType<T> | undefined;
437
+ cancel: () => void;
438
+ flush: () => ReturnType<T> | undefined;
439
+ };
440
+
441
+ /**
442
+ * 创建防抖函数,延迟调用 `func` 直到自上次调用以来已经过了 `wait` 毫秒。
443
+ *
444
+ * @template T - 要防抖的函数类型
445
+ * @param func - 要防抖的函数
446
+ * @param [wait=0] - 延迟时间(毫秒)
447
+ * @param [options] - 防抖配置选项
448
+ * @param [options.leading=false] - 是否在超时开始时调用(前沿触发)
449
+ * @param [options.trailing=true] - 是否在超时结束时调用(后沿触发)
450
+ * @param [options.maxWait] - 函数被延迟调用的最大时间
451
+ * @returns 新的防抖函数,包含 cancel 和 flush 方法
452
+ */
453
+ export function debounce<T extends AnyFunction>(
454
+ func: T,
455
+ wait = 0,
456
+ options: DebounceOptions = {}
457
+ ): DebouncedFunction<T> {
458
+ if (typeof func !== 'function') {
459
+ throw new TypeError('Expected a function');
460
+ }
461
+
462
+ const delay = Number(wait) || 0;
463
+ const settings = options && typeof options === 'object' ? options : {};
464
+ const leading = !!settings.leading;
465
+ const trailing = hasOwn(settings, 'trailing') ? !!settings.trailing : true;
466
+ const maxing = hasOwn(settings, 'maxWait');
467
+ const maxWait = maxing ? Math.max(Number(settings.maxWait) || 0, delay) : 0;
468
+
469
+ let pendingCall: PendingCall<T> | undefined;
470
+ let result: ReturnType<T> | undefined;
471
+ let timerId: ReturnType<typeof setTimeout> | undefined;
472
+ let lastCallTime: number | undefined;
473
+ let lastInvokeTime = 0;
474
+
475
+ function invokeFunc(time: number): ReturnType<T> | undefined {
476
+ const call = pendingCall;
477
+ if (!call) return result;
478
+ pendingCall = undefined;
479
+ lastInvokeTime = time;
480
+ result = Reflect.apply(func, call.receiver, call.args) as ReturnType<T>;
481
+ return result;
482
+ }
483
+
484
+ function leadingEdge(time: number): ReturnType<T> | undefined {
485
+ lastInvokeTime = time;
486
+ timerId = setTimeout(timerExpired, delay);
487
+ return leading ? invokeFunc(time) : result;
488
+ }
489
+
490
+ function remainingWait(time: number): number {
491
+ const timeSinceLastCall = time - (lastCallTime || 0);
492
+ const timeSinceLastInvoke = time - lastInvokeTime;
493
+ const timeWaiting = delay - timeSinceLastCall;
494
+ return maxing
495
+ ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
496
+ : timeWaiting;
497
+ }
498
+
499
+ function shouldInvoke(time: number): boolean {
500
+ const timeSinceLastCall = time - (lastCallTime || 0);
501
+ const timeSinceLastInvoke = time - lastInvokeTime;
502
+ return (
503
+ lastCallTime === undefined ||
504
+ timeSinceLastCall >= delay ||
505
+ timeSinceLastCall < 0 ||
506
+ (maxing && timeSinceLastInvoke >= maxWait)
507
+ );
508
+ }
509
+
510
+ function timerExpired(): ReturnType<T> | undefined {
511
+ const time = Date.now();
512
+ if (shouldInvoke(time)) {
513
+ return trailingEdge(time);
514
+ }
515
+ timerId = setTimeout(timerExpired, remainingWait(time));
516
+ }
517
+
518
+ function trailingEdge(time: number): ReturnType<T> | undefined {
519
+ timerId = undefined;
520
+ if (trailing && pendingCall) {
521
+ return invokeFunc(time);
522
+ }
523
+ pendingCall = undefined;
524
+ return result;
525
+ }
526
+
527
+ function cancel(): void {
528
+ if (timerId !== undefined) {
529
+ clearTimeout(timerId);
530
+ }
531
+ lastInvokeTime = 0;
532
+ lastCallTime = undefined;
533
+ timerId = undefined;
534
+ pendingCall = undefined;
535
+ }
536
+
537
+ function flush(): ReturnType<T> | undefined {
538
+ return timerId === undefined ? result : trailingEdge(Date.now());
539
+ }
540
+
541
+ function debounced(
542
+ this: ThisParameterType<T>,
543
+ ...args: Parameters<T>
544
+ ): ReturnType<T> | undefined {
545
+ const time = Date.now();
546
+ const isInvoking = shouldInvoke(time);
547
+ pendingCall = {
548
+ receiver: this,
549
+ args,
550
+ };
551
+ lastCallTime = time;
552
+
553
+ if (isInvoking) {
554
+ if (timerId === undefined) {
555
+ return leadingEdge(lastCallTime);
556
+ }
557
+ if (maxing) {
558
+ timerId = setTimeout(timerExpired, delay);
559
+ return invokeFunc(lastCallTime);
560
+ }
561
+ }
562
+ if (timerId === undefined) {
563
+ timerId = setTimeout(timerExpired, delay);
564
+ }
565
+ return result;
566
+ }
567
+
568
+ debounced.cancel = cancel;
569
+ debounced.flush = flush;
570
+ return debounced;
571
+ }
572
+
573
+ /**
574
+ * 创建节流函数,确保在指定时间间隔内函数只执行一次。
575
+ *
576
+ * 节流原理:在连续触发事件时,函数在指定时间间隔内只会执行一次。
577
+ * 适用于滚动事件、鼠标移动事件等需要限制调用频率的场景。
578
+ *
579
+ * @template T - 要节流的函数类型
580
+ * @param func - 要节流的函数
581
+ * @param [wait=0] - 时间间隔(毫秒),在此期间函数只会执行一次
582
+ * @param [options] - 节流配置选项
583
+ * @param [options.leading=true] - 是否在时间间隔开始时调用(前沿触发)
584
+ * @param [options.trailing=true] - 是否在时间间隔结束时调用(后沿触发)
585
+ * @returns 新的节流函数,包含 cancel 和 flush 方法
586
+ */
587
+ export function throttle<T extends AnyFunction>(
588
+ func: T,
589
+ wait = 0,
590
+ options: DebounceOptions = {}
591
+ ): DebouncedFunction<T> {
592
+ if (typeof func !== 'function') {
593
+ throw new TypeError('Expected a function');
594
+ }
595
+ const settings = options && typeof options === 'object' ? options : {};
596
+ const leading = hasOwn(settings, 'leading') ? !!settings.leading : true;
597
+ const trailing = hasOwn(settings, 'trailing') ? !!settings.trailing : true;
598
+ return debounce(func, wait, { leading, trailing, maxWait: wait });
599
+ }
600
+
601
+ interface DefaultRule {
602
+ default?: unknown;
603
+ factory?: boolean;
604
+ }
605
+
606
+ function resolveDefault(rule: DefaultRule): unknown {
607
+ if (!hasOwn(rule, 'default')) return undefined;
608
+ const defaultValue = rule.default;
609
+ const value =
610
+ rule.factory && typeof defaultValue === 'function'
611
+ ? defaultValue()
612
+ : defaultValue;
613
+ return cloneDefault(value);
614
+ }