vanilla-jui 1.3.8 → 1.3.10
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.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -2
- package/src/components/accordion.js +373 -0
- package/src/components/components.js +0 -0
- package/src/components/drop.js +437 -0
- package/src/components/flow.js +1133 -0
- package/src/components/form.js +585 -0
- package/src/components/icons.js +83 -0
- package/src/components/menu.js +422 -0
- package/src/components/modal.js +1174 -0
- package/src/components/offcanvas.js +332 -0
- package/src/components/pagination.js +536 -0
- package/src/components/parabola.js +295 -0
- package/src/components/sticky.js +271 -0
- package/src/components/swiper.js +1123 -0
- package/src/components/tabs.js +746 -0
- package/src/components/theme.js +353 -0
- package/src/components/toast.js +329 -0
- package/src/components/toc.js +226 -0
- package/src/components/tooltip.js +86 -0
- package/src/components/validator.js +586 -0
- package/src/core/Component.js +248 -0
- package/src/css/accordion.css +72 -0
- package/src/css/animation.css +177 -0
- package/src/css/avatar.css +62 -0
- package/src/css/badge.css +66 -0
- package/src/css/breadcrumb.css +106 -0
- package/src/css/button.css +296 -0
- package/src/css/card.css +151 -0
- package/src/css/color.css +1176 -0
- package/src/css/color_p3.css +1184 -0
- package/src/css/components.css +26 -0
- package/src/css/content.css +716 -0
- package/src/css/divider.css +9 -0
- package/src/css/drop.css +28 -0
- package/src/css/flow.css +86 -0
- package/src/css/font.css +23 -0
- package/src/css/form.css +919 -0
- package/src/css/gradient.css +211 -0
- package/src/css/icon.css +63 -0
- package/src/css/index.css +12 -0
- package/src/css/layout.css +782 -0
- package/src/css/loading.css +38 -0
- package/src/css/menu.css +564 -0
- package/src/css/modal.css +111 -0
- package/src/css/offcanvas.css +104 -0
- package/src/css/pagination.css +25 -0
- package/src/css/popup.css +69 -0
- package/src/css/skeleton.css +76 -0
- package/src/css/status.css +56 -0
- package/src/css/swiper.css +160 -0
- package/src/css/table.css +52 -0
- package/src/css/tabs.css +95 -0
- package/src/css/tag.css +98 -0
- package/src/css/theme-palette.css +59 -0
- package/src/css/tip.css +84 -0
- package/src/css/toast.css +154 -0
- package/src/css/toast.min.css +1 -0
- package/src/css/toc.css +38 -0
- package/src/css/tokens.css +1324 -0
- package/src/css/toolbar.css +115 -0
- package/src/css/tooltip.css +19 -0
- package/src/icons/index.js +30 -0
- package/src/index.js +45 -0
- package/src/locales/index.js +45 -0
- package/src/locales/index.min.js +1 -0
- package/src/utilities/browser.js +111 -0
- package/src/utilities/cache.js +159 -0
- package/src/utilities/core.js +502 -0
- package/src/utilities/debug.js +103 -0
- package/src/utilities/dom.js +313 -0
- package/src/utilities/events.js +69 -0
- package/src/utilities/http.js +30 -0
- package/src/utilities/legacy-dom.js +179 -0
- package/src/utilities/storage.js +46 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断对象是否包含指定属性。
|
|
3
|
+
* @param {object} obj - 待检查的对象。
|
|
4
|
+
* @param {string} key - 属性名。
|
|
5
|
+
* @returns {boolean} - 如果对象包含指定属性,则返回 true;否则返回 false。
|
|
6
|
+
*/
|
|
7
|
+
export const hasOwn = (obj, key) =>
|
|
8
|
+
Object.prototype.hasOwnProperty.call(obj, key);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 创建一个去重后的数组,并移除“假值”(如 null、undefined、false、0、NaN、空字符串)。
|
|
12
|
+
* @param {Array} list - 待去重的数组。
|
|
13
|
+
* @returns {Array} - 去重后的数组。
|
|
14
|
+
*/
|
|
15
|
+
// export const uniq = (list) => Array.from(new Set(list.filter(Boolean)));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 定时器管理器
|
|
19
|
+
*/
|
|
20
|
+
export const timer = {
|
|
21
|
+
// 存储定时器 { key: timerId }
|
|
22
|
+
timers: {},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 注册并开始一个定时器
|
|
26
|
+
* @param {string} key - 定时器的唯一标识
|
|
27
|
+
* @param {number} duration - 延迟执行的时间(毫秒)
|
|
28
|
+
* @param {function} callback - 延迟执行的回调函数
|
|
29
|
+
*/
|
|
30
|
+
start(key, duration, callback) {
|
|
31
|
+
// 1. 如果该 key 已经存在,先清除旧的定时器,防止重复触发
|
|
32
|
+
if (this.timers[key]) {
|
|
33
|
+
clearTimeout(this.timers[key]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. 设置新的定时器,并保存其 ID
|
|
37
|
+
this.timers[key] = setTimeout(() => {
|
|
38
|
+
callback();
|
|
39
|
+
// 3. 执行完毕后,自动从管理器中注销该定时器
|
|
40
|
+
delete this.timers[key];
|
|
41
|
+
}, duration);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 明确注销一个定时器
|
|
46
|
+
* @param {string} key - 需要取消的定时器唯一标识
|
|
47
|
+
*/
|
|
48
|
+
cancel(key) {
|
|
49
|
+
if (this.timers[key]) {
|
|
50
|
+
clearTimeout(this.timers[key]);
|
|
51
|
+
// 从管理器中移除该记录
|
|
52
|
+
delete this.timers[key];
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 获取值的增强类型名称。
|
|
59
|
+
* @param {*} val 需要判断类型的值。
|
|
60
|
+
* @returns {string} 类型名称,如 array、null、HTMLElement、Node、string 等。
|
|
61
|
+
*/
|
|
62
|
+
export const getType = (val) => {
|
|
63
|
+
if (val === null) return 'null';
|
|
64
|
+
if (Array.isArray(val)) return 'array';
|
|
65
|
+
if (typeof HTMLElement !== 'undefined' && val instanceof HTMLElement) {
|
|
66
|
+
return 'HTMLElement';
|
|
67
|
+
}
|
|
68
|
+
if (typeof Node !== 'undefined' && val instanceof Node) return 'Node';
|
|
69
|
+
return typeof val;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 判断是否为普通可构造函数。
|
|
74
|
+
* @param {*} fn 需要判断的值。
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
export const isFunction = (fn) => {
|
|
78
|
+
return (
|
|
79
|
+
typeof fn === 'function' &&
|
|
80
|
+
fn.prototype !== null &&
|
|
81
|
+
fn.prototype.constructor === fn
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 判断是否为类。
|
|
87
|
+
* @param {Function} fn 函数。
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export const isClass = (fn) => {
|
|
91
|
+
return (
|
|
92
|
+
typeof fn === 'function' &&
|
|
93
|
+
/^class\s/.test(Function.prototype.toString.call(fn))
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 判断是否为普通对象。
|
|
99
|
+
* @param {*} value 需要判断的值。
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function isPlainObject(value) {
|
|
103
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
104
|
+
const proto = Object.getPrototypeOf(value);
|
|
105
|
+
return proto === Object.prototype || proto === null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 浅拷贝默认值。
|
|
110
|
+
* @param {*} value 需要拷贝的值。
|
|
111
|
+
* @returns {*} 拷贝后的值。
|
|
112
|
+
*/
|
|
113
|
+
function cloneDefault(value) {
|
|
114
|
+
if (Array.isArray(value)) return value.slice();
|
|
115
|
+
if (isPlainObject(value)) return { ...value };
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 规范化规则对象。
|
|
121
|
+
* @param {string|string[]|object} rule 需要规范化的规则对象。
|
|
122
|
+
* @returns {object} 规范化后的规则对象。
|
|
123
|
+
*/
|
|
124
|
+
function normalizeRule(rule = {}) {
|
|
125
|
+
if (typeof rule === 'string' || Array.isArray(rule)) return { type: rule };
|
|
126
|
+
if (!rule || typeof rule !== 'object') return {};
|
|
127
|
+
return rule;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 运行错误条件。
|
|
132
|
+
* @param {string} name 参数名称,用于错误信息。
|
|
133
|
+
* @param {*} value 参数值。
|
|
134
|
+
* @param {Function|Array<Function|{test:Function,message?:string}>} [conditions=[]] 附加条件。
|
|
135
|
+
* @throws {Error} 条件失败时抛出。
|
|
136
|
+
*/
|
|
137
|
+
function runValidateConditions(name, value, conditions = []) {
|
|
138
|
+
const list = Array.isArray(conditions)
|
|
139
|
+
? conditions
|
|
140
|
+
: conditions
|
|
141
|
+
? [conditions]
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
for (const c of list) {
|
|
145
|
+
let testFn;
|
|
146
|
+
let message;
|
|
147
|
+
|
|
148
|
+
if (typeof c === 'function') {
|
|
149
|
+
testFn = c;
|
|
150
|
+
message = 'does not satisfy the required condition.';
|
|
151
|
+
} else if (c && typeof c.test === 'function') {
|
|
152
|
+
testFn = c.test;
|
|
153
|
+
message = c.message || 'condition failed.';
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'Validator: Condition must be a function or { test, message }.'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!testFn(value)) {
|
|
161
|
+
throw new Error(`Validator: ${name} ${message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 格式化错误名称。
|
|
168
|
+
* @param {string} namespace 命名空间。
|
|
169
|
+
* @param {string} name 参数名。
|
|
170
|
+
* @returns {string} 格式化后的错误名称。
|
|
171
|
+
*/
|
|
172
|
+
function formatValidateName(namespace, name) {
|
|
173
|
+
return namespace ? `${namespace}.${name}` : name;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveDefault(rule) {
|
|
177
|
+
if (!hasOwn(rule, 'default')) return undefined;
|
|
178
|
+
const value =
|
|
179
|
+
rule.factory && typeof rule.default === 'function'
|
|
180
|
+
? rule.default()
|
|
181
|
+
: rule.default;
|
|
182
|
+
return cloneDefault(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 按规则校验参数。
|
|
187
|
+
*
|
|
188
|
+
* rule 可包含 type/types、required、enum、conditions、validate、message 等字段。
|
|
189
|
+
* @param {string} name 参数名。
|
|
190
|
+
* @param {*} value 参数值。
|
|
191
|
+
* @param {string|string[]|object} [rule={}] 校验规则。
|
|
192
|
+
* @param {string} [namespace=""] 错误命名空间。
|
|
193
|
+
* @returns {*} 校验通过后的原值。
|
|
194
|
+
* @throws {Error} 校验失败时抛出。
|
|
195
|
+
*/
|
|
196
|
+
export function validateParam(name, value, rule = {}, namespace = '') {
|
|
197
|
+
// if (JUI_DEBUG_DISABLED) return value;
|
|
198
|
+
|
|
199
|
+
const config = normalizeRule(rule);
|
|
200
|
+
const label = formatValidateName(namespace, name);
|
|
201
|
+
const expectedTypes = hasOwn(config, 'types') ? config.types : config.type;
|
|
202
|
+
|
|
203
|
+
if (config.required && (value === undefined || value === null)) {
|
|
204
|
+
throw new Error(`Validator: ${label} is required.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (expectedTypes !== undefined) {
|
|
208
|
+
const types = Array.isArray(expectedTypes)
|
|
209
|
+
? expectedTypes
|
|
210
|
+
: [expectedTypes];
|
|
211
|
+
const actualType = getType(value);
|
|
212
|
+
const typeMatch = types.some((t) => t === actualType);
|
|
213
|
+
|
|
214
|
+
if (!typeMatch) {
|
|
215
|
+
const allowed = types.join(', ');
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Validator: ${label} expects ${allowed}, but got ${actualType}.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
runValidateConditions(label, value, config.conditions);
|
|
222
|
+
} else {
|
|
223
|
+
runValidateConditions(label, value, config.conditions);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(config.enum) && !config.enum.includes(value)) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Validator: ${label} expects one of ${config.enum.join(', ')}.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof config.validate === 'function') {
|
|
233
|
+
const valid = config.validate(value);
|
|
234
|
+
|
|
235
|
+
if (!valid) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Validator: ${label} ${config.message || 'does not satisfy the required condition.'}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 合并默认值、执行 normalize 并校验配置。
|
|
247
|
+
*
|
|
248
|
+
* schema 的每一项可同时定义 default、factory、normalize 和校验规则。
|
|
249
|
+
* @param {object} [input={}] 用户传入配置。
|
|
250
|
+
* @param {Record<string, object|string|string[]>} [schema={}] 配置 schema。
|
|
251
|
+
* @param {string} [namespace="Options"] 错误命名空间。
|
|
252
|
+
* @returns {object} 合并并校验后的配置。
|
|
253
|
+
*/
|
|
254
|
+
export function resolveProps(input = {}, schema = {}, namespace = 'Options') {
|
|
255
|
+
const source = input == null ? {} : input;
|
|
256
|
+
|
|
257
|
+
// if (
|
|
258
|
+
// !JUI_DEBUG_DISABLED &&
|
|
259
|
+
// (typeof source !== 'object' || Array.isArray(source))
|
|
260
|
+
// ) {
|
|
261
|
+
if (typeof source !== 'object' || Array.isArray(source)) {
|
|
262
|
+
throw new Error(`${namespace} expects object.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// const resolved = { ...(source && typeof source === 'object' ? source : {}) };
|
|
266
|
+
const resolved = { ...source };
|
|
267
|
+
const entries = Object.entries(schema || {});
|
|
268
|
+
|
|
269
|
+
for (const [key, rawRule] of entries) {
|
|
270
|
+
const rule = normalizeRule(rawRule);
|
|
271
|
+
resolved[key] = hasOwn(source, key) ? source[key] : resolveDefault(rule);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const [key, rawRule] of entries) {
|
|
275
|
+
const rule = normalizeRule(rawRule);
|
|
276
|
+
if (typeof rule.normalize === 'function') {
|
|
277
|
+
resolved[key] = rule.normalize(resolved[key], {
|
|
278
|
+
key,
|
|
279
|
+
input: source,
|
|
280
|
+
options: resolved,
|
|
281
|
+
schema,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// if (!JUI_DEBUG_DISABLED) {
|
|
287
|
+
// for (const [key, rule] of Object.entries(schema || {})) {
|
|
288
|
+
// validateParam(key, resolved[key], rule, namespace);
|
|
289
|
+
// }
|
|
290
|
+
// }
|
|
291
|
+
for (const [key, rule] of Object.entries(schema || {})) {
|
|
292
|
+
validateParam(key, resolved[key], rule, namespace);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return resolved;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 生成标准 UUID。
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
export function uuid() {
|
|
303
|
+
if (typeof crypto.randomUUID === 'function') {
|
|
304
|
+
return crypto.randomUUID();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const bytes = new Uint8Array(16);
|
|
308
|
+
if (typeof crypto.getRandomValues !== 'function') {
|
|
309
|
+
throw new Error('Your browser is too old to support secure login.');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
crypto.getRandomValues(bytes);
|
|
313
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
314
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
315
|
+
|
|
316
|
+
const hex = Array.from(bytes)
|
|
317
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
318
|
+
.join('');
|
|
319
|
+
|
|
320
|
+
return [
|
|
321
|
+
hex.slice(0, 8),
|
|
322
|
+
hex.slice(8, 12),
|
|
323
|
+
hex.slice(12, 16),
|
|
324
|
+
hex.slice(16, 20),
|
|
325
|
+
hex.slice(20, 32),
|
|
326
|
+
].join('-');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 生成适合 DOM id 的随机字符串。
|
|
331
|
+
* @param {number} [length=8] 字符串长度,范围 1 到 32。
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
export function randomId(length = 8) {
|
|
335
|
+
if (!Number.isInteger(length) || length < 1 || length > 32) {
|
|
336
|
+
throw new Error('Length must be an integer between 1 and 32');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const chars = 'abcdefghijkmnpqrstuvwxyz23456789';
|
|
340
|
+
const bytes = new Uint8Array(length);
|
|
341
|
+
crypto.getRandomValues(bytes);
|
|
342
|
+
|
|
343
|
+
let result = '';
|
|
344
|
+
for (let i = 0; i < length; i++) {
|
|
345
|
+
result += chars[bytes[i] & 31];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 创建防抖函数,确保在连续调用时,只有在指定的等待时间后才会执行一次。
|
|
353
|
+
* @param {Function} func 要防抖的函数
|
|
354
|
+
* @param {number} wait 等待时间,单位为毫秒
|
|
355
|
+
* @param {Object} options 配置选项
|
|
356
|
+
* @param {boolean} [options.leading=false] 是否在等待时间的开始调用函数
|
|
357
|
+
* @param {boolean} [options.trailing=true] 是否在等待时间的结束调用函数
|
|
358
|
+
* @param {number} [options.maxWait] 最大等待时间,确保函数在最长等待时间内至少执行一次
|
|
359
|
+
* @returns {Function} 返回一个新的防抖函数
|
|
360
|
+
*/
|
|
361
|
+
export function debounce(func, wait = 0, options = {}) {
|
|
362
|
+
if (typeof func !== 'function') {
|
|
363
|
+
throw new TypeError('Expected a function');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const delay = Number(wait) || 0;
|
|
367
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
368
|
+
const leading = !!settings.leading;
|
|
369
|
+
const trailing = hasOwn(settings, 'trailing') ? !!settings.trailing : true;
|
|
370
|
+
const maxing = hasOwn(settings, 'maxWait');
|
|
371
|
+
const maxWait = maxing ? Math.max(Number(settings.maxWait) || 0, delay) : 0;
|
|
372
|
+
|
|
373
|
+
let lastArgs;
|
|
374
|
+
let lastThis;
|
|
375
|
+
let result;
|
|
376
|
+
let timerId;
|
|
377
|
+
let lastCallTime;
|
|
378
|
+
let lastInvokeTime = 0;
|
|
379
|
+
|
|
380
|
+
function invokeFunc(time) {
|
|
381
|
+
const args = lastArgs;
|
|
382
|
+
const thisArg = lastThis;
|
|
383
|
+
lastArgs = undefined;
|
|
384
|
+
lastThis = undefined;
|
|
385
|
+
lastInvokeTime = time;
|
|
386
|
+
result = func.apply(thisArg, args);
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function leadingEdge(time) {
|
|
391
|
+
lastInvokeTime = time;
|
|
392
|
+
timerId = setTimeout(timerExpired, delay);
|
|
393
|
+
return leading ? invokeFunc(time) : result;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function remainingWait(time) {
|
|
397
|
+
const timeSinceLastCall = time - lastCallTime;
|
|
398
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
399
|
+
const timeWaiting = delay - timeSinceLastCall;
|
|
400
|
+
return maxing
|
|
401
|
+
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
|
|
402
|
+
: timeWaiting;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function shouldInvoke(time) {
|
|
406
|
+
const timeSinceLastCall = time - lastCallTime;
|
|
407
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
lastCallTime === undefined ||
|
|
411
|
+
timeSinceLastCall >= delay ||
|
|
412
|
+
timeSinceLastCall < 0 ||
|
|
413
|
+
(maxing && timeSinceLastInvoke >= maxWait)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function timerExpired() {
|
|
418
|
+
const time = Date.now();
|
|
419
|
+
if (shouldInvoke(time)) {
|
|
420
|
+
return trailingEdge(time);
|
|
421
|
+
}
|
|
422
|
+
timerId = setTimeout(timerExpired, remainingWait(time));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function trailingEdge(time) {
|
|
426
|
+
timerId = undefined;
|
|
427
|
+
|
|
428
|
+
if (trailing && lastArgs) {
|
|
429
|
+
return invokeFunc(time);
|
|
430
|
+
}
|
|
431
|
+
lastArgs = undefined;
|
|
432
|
+
lastThis = undefined;
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function cancel() {
|
|
437
|
+
if (timerId !== undefined) {
|
|
438
|
+
clearTimeout(timerId);
|
|
439
|
+
}
|
|
440
|
+
lastInvokeTime = 0;
|
|
441
|
+
lastCallTime = undefined;
|
|
442
|
+
timerId = undefined;
|
|
443
|
+
lastArgs = undefined;
|
|
444
|
+
lastThis = undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function flush() {
|
|
448
|
+
return timerId === undefined ? result : trailingEdge(Date.now());
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function debounced(...args) {
|
|
452
|
+
const time = Date.now();
|
|
453
|
+
const isInvoking = shouldInvoke(time);
|
|
454
|
+
|
|
455
|
+
lastArgs = args;
|
|
456
|
+
lastThis = this;
|
|
457
|
+
lastCallTime = time;
|
|
458
|
+
|
|
459
|
+
if (isInvoking) {
|
|
460
|
+
if (timerId === undefined) {
|
|
461
|
+
return leadingEdge(lastCallTime);
|
|
462
|
+
}
|
|
463
|
+
if (maxing) {
|
|
464
|
+
timerId = setTimeout(timerExpired, delay);
|
|
465
|
+
return invokeFunc(lastCallTime);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (timerId === undefined) {
|
|
470
|
+
timerId = setTimeout(timerExpired, delay);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
debounced.cancel = cancel;
|
|
476
|
+
debounced.flush = flush;
|
|
477
|
+
return debounced;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 创建节流函数,确保在指定时间间隔内只执行一次。
|
|
482
|
+
* 该函数基于防抖函数实现,适用于需要限制函数调用频率的场景。
|
|
483
|
+
* @param {Function} func
|
|
484
|
+
* @param {number} wait
|
|
485
|
+
* @param {Object} options
|
|
486
|
+
* @returns {Function}
|
|
487
|
+
*/
|
|
488
|
+
export function throttle(func, wait = 0, options = {}) {
|
|
489
|
+
if (typeof func !== 'function') {
|
|
490
|
+
throw new TypeError('Expected a function');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
494
|
+
const leading = hasOwn(settings, 'leading') ? !!settings.leading : true;
|
|
495
|
+
const trailing = hasOwn(settings, 'trailing') ? !!settings.trailing : true;
|
|
496
|
+
|
|
497
|
+
return debounce(func, wait, {
|
|
498
|
+
leading,
|
|
499
|
+
trailing,
|
|
500
|
+
maxWait: wait,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const JUI_DEBUG_DISABLED =
|
|
2
|
+
typeof __JUI__ !== 'undefined' && __JUI__.debug === false;
|
|
3
|
+
|
|
4
|
+
const NOOP = () => {};
|
|
5
|
+
|
|
6
|
+
let activeCleanup = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 判断当前构建是否启用调试验证。
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isDebug() {
|
|
13
|
+
return !JUI_DEBUG_DISABLED;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判断当前构建是否需要启用防调试守卫。
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function shouldPreventDebugger() {
|
|
21
|
+
return (
|
|
22
|
+
JUI_DEBUG_DISABLED &&
|
|
23
|
+
typeof window !== 'undefined' &&
|
|
24
|
+
typeof window.setInterval === 'function'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 启用防调试守卫。
|
|
30
|
+
*
|
|
31
|
+
* 守卫通过 debugger 暂停造成的时间差判断调试行为;不会使用死循环阻塞页面。
|
|
32
|
+
* @param {object} [options={}] 守卫配置。
|
|
33
|
+
* @param {number} [options.interval=1000] 检测间隔,单位毫秒。
|
|
34
|
+
* @param {number} [options.threshold=120] 触发判定的暂停阈值,单位毫秒。
|
|
35
|
+
* @param {'blank'|'reload'|'throw'|Function} [options.action='blank'] 触发后的动作。
|
|
36
|
+
* @param {string} [options.redirect='about:blank'] action 为 blank 时跳转地址。
|
|
37
|
+
* @returns {Function} 清理函数;未启用时返回空函数。
|
|
38
|
+
*/
|
|
39
|
+
export function preventDebugger(options = {}) {
|
|
40
|
+
if (!shouldPreventDebugger()) return NOOP;
|
|
41
|
+
if (activeCleanup) return activeCleanup;
|
|
42
|
+
|
|
43
|
+
const interval = normalizeNumber(options.interval, 1000, 100);
|
|
44
|
+
const threshold = normalizeNumber(options.threshold, 120, 30);
|
|
45
|
+
const action = options.action || 'blank';
|
|
46
|
+
const redirect = options.redirect || 'about:blank';
|
|
47
|
+
|
|
48
|
+
let disposed = false;
|
|
49
|
+
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
disposed = true;
|
|
52
|
+
window.clearInterval(timer);
|
|
53
|
+
activeCleanup = null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const detect = () => {
|
|
57
|
+
if (disposed) return;
|
|
58
|
+
|
|
59
|
+
const startedAt = Date.now();
|
|
60
|
+
runDebuggerStatement();
|
|
61
|
+
const elapsed = Date.now() - startedAt;
|
|
62
|
+
|
|
63
|
+
if (elapsed >= threshold) {
|
|
64
|
+
cleanup();
|
|
65
|
+
handleDebuggerDetected(action, redirect, elapsed);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const timer = window.setInterval(detect, interval);
|
|
70
|
+
activeCleanup = cleanup;
|
|
71
|
+
detect();
|
|
72
|
+
|
|
73
|
+
return cleanup;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeNumber(value, fallback, min) {
|
|
77
|
+
return Number.isFinite(value) && value >= min ? value : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runDebuggerStatement() {
|
|
81
|
+
// eslint-disable-next-line no-debugger
|
|
82
|
+
debugger;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleDebuggerDetected(action, redirect, elapsed) {
|
|
86
|
+
if (typeof action === 'function') {
|
|
87
|
+
action({ elapsed });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === 'reload') {
|
|
92
|
+
window.location.reload();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (action === 'throw') {
|
|
97
|
+
throw new Error('JUI: debugger detected.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.location.replace(redirect);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// preventDebugger();
|