ph-utils 0.20.0 → 1.0.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.
- package/lib/browser.d.ts +416 -0
- package/lib/browser.js +858 -0
- package/lib/color-CUZrwrjD.js +206 -0
- package/lib/index.d.ts +615 -61
- package/lib/index.js +1539 -235
- package/lib/node.d.ts +115 -0
- package/lib/node.js +213 -0
- package/package.json +10 -64
- package/lib/array.d.ts +0 -79
- package/lib/array.js +0 -212
- package/lib/color.d.ts +0 -55
- package/lib/color.js +0 -294
- package/lib/config.d.ts +0 -33
- package/lib/config.js +0 -99
- package/lib/copy.d.ts +0 -11
- package/lib/copy.js +0 -101
- package/lib/crypto.d.ts +0 -74
- package/lib/crypto.js +0 -261
- package/lib/crypto_node.d.ts +0 -61
- package/lib/crypto_node.js +0 -133
- package/lib/date.d.ts +0 -66
- package/lib/date.js +0 -202
- package/lib/dom.d.ts +0 -265
- package/lib/dom.js +0 -635
- package/lib/file.d.ts +0 -29
- package/lib/file.js +0 -54
- package/lib/id.d.ts +0 -68
- package/lib/id.js +0 -170
- package/lib/logger.d.ts +0 -62
- package/lib/logger.js +0 -122
- package/lib/server.d.ts +0 -33
- package/lib/server.js +0 -65
- package/lib/storage.d.ts +0 -51
- package/lib/storage.js +0 -73
- package/lib/theme.d.ts +0 -44
- package/lib/theme.js +0 -156
- package/lib/validator.d.ts +0 -71
- package/lib/validator.js +0 -248
- package/lib/web.d.ts +0 -30
- package/lib/web.js +0 -100
package/lib/browser.js
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
import { t as adjust } from "./color-CUZrwrjD.js";
|
|
2
|
+
//#region src/dom.ts
|
|
3
|
+
function elem(selector, dom) {
|
|
4
|
+
if (typeof selector === "string") return (dom || document).querySelectorAll(selector);
|
|
5
|
+
else return [selector];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 根据选择器获取 DOM 元素。
|
|
9
|
+
* @param selector - 选择器字符串或 HTMLElement 实例。
|
|
10
|
+
* @param dom - 可选参数,指定在哪个 DOM 节点下查找元素,默认为 document。
|
|
11
|
+
* @returns 返回匹配到的 HTMLElement 实例。
|
|
12
|
+
*/
|
|
13
|
+
function $(selector, dom) {
|
|
14
|
+
return elem(selector, dom);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 创建一个 HTML 元素,支持通过标签名或 HTML 字符串创建。
|
|
18
|
+
* @param tag - 元素标签名或 HTML 字符串。
|
|
19
|
+
* @param option - 元素的属性、样式、文本内容等配置。所有不是指定的属性,都会通过 setAttribute 设置
|
|
20
|
+
* @param children - 子节点
|
|
21
|
+
* @returns 创建的 HTML 元素。
|
|
22
|
+
*/
|
|
23
|
+
function create(tag, option = {}, children) {
|
|
24
|
+
let $el;
|
|
25
|
+
if (tag.startsWith("<") && tag.endsWith(">")) $el = new DOMParser().parseFromString(tag, "text/html").body.firstElementChild;
|
|
26
|
+
else $el = document.createElement(tag);
|
|
27
|
+
if ($el) {
|
|
28
|
+
if (option) for (const key in option) {
|
|
29
|
+
const value = option[key];
|
|
30
|
+
if (value != null) {
|
|
31
|
+
if (key === "class") $el.className = formatClass(value);
|
|
32
|
+
else if (key === "style") $el.style.cssText = formatStyle(value);
|
|
33
|
+
else if (key === "textContent" && value) $el.textContent = value;
|
|
34
|
+
else if (key === "innerHTML" && value) $el.innerHTML = value;
|
|
35
|
+
else if (key === "outerHTML" && value) $el.outerHTML = value;
|
|
36
|
+
else if (typeof value === "boolean" && value === true) $el.setAttribute(key, "");
|
|
37
|
+
else $el.setAttribute(key, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (children) {
|
|
41
|
+
if (children instanceof HTMLElement || children instanceof DocumentFragment) $el.appendChild(children);
|
|
42
|
+
else {
|
|
43
|
+
let len = children.length;
|
|
44
|
+
const fragment = document.createDocumentFragment();
|
|
45
|
+
for (let i = 0; i < len; i++) {
|
|
46
|
+
const child = children[i];
|
|
47
|
+
if (child instanceof HTMLElement) fragment.appendChild(child);
|
|
48
|
+
}
|
|
49
|
+
if (len > 0) $el.appendChild(fragment);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return $el;
|
|
54
|
+
}
|
|
55
|
+
/** 创建节点 */
|
|
56
|
+
function $$(tag, option = {}, children) {
|
|
57
|
+
return create(tag, option, children);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 根据选择器获取匹配的第一个 DOM 元素。
|
|
61
|
+
* @param selector - 选择器字符串或直接的 HTMLElement。
|
|
62
|
+
* @param dom - 可选的父级 DOM 元素,默认为当前文档。
|
|
63
|
+
* @returns 返回匹配的第一个 HTMLElement,如果没有找到则返回 null。
|
|
64
|
+
*/
|
|
65
|
+
function $one(selector, dom) {
|
|
66
|
+
if (typeof selector === "string") return (dom || document).querySelector(selector);
|
|
67
|
+
return selector;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 为节点添加 class
|
|
71
|
+
* @param {HTMLElement} elem 待添加 class 的节点
|
|
72
|
+
* @param {string} clazz 需要添加的 class
|
|
73
|
+
*/
|
|
74
|
+
function addClass(elem, clazz) {
|
|
75
|
+
elem.classList.add(clazz);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 节点移除 class
|
|
79
|
+
* @param {HTMLElement} elem 待移除 class 的节点
|
|
80
|
+
* @param {string} clazz 需要移除的 class
|
|
81
|
+
*/
|
|
82
|
+
function removeClass(elem, clazz) {
|
|
83
|
+
elem.classList.remove(clazz);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 判断节点是否包含某个 class
|
|
87
|
+
* @param elem 待判断 class 的节点
|
|
88
|
+
* @param clazz 待判断的 class
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
function hasClass(elem, clazz) {
|
|
92
|
+
return elem.classList.contains(clazz);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 切换指定元素的类名。
|
|
96
|
+
* 如果元素已包含该类名,则移除它;否则添加它。
|
|
97
|
+
* @param el - 要操作的 HTML 元素。
|
|
98
|
+
* @param clazz - 要切换的类名。
|
|
99
|
+
*/
|
|
100
|
+
function toggleClass(el, clazz) {
|
|
101
|
+
if (hasClass(el, clazz)) removeClass(el, clazz);
|
|
102
|
+
else addClass(el, clazz);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 替换指定元素的 CSS 类
|
|
106
|
+
*
|
|
107
|
+
* 该函数用于根据给定的旧类名或类索引,将其替换为新类名。
|
|
108
|
+
* 如果旧类不存在,则不会进行替换。
|
|
109
|
+
*
|
|
110
|
+
* @param el - 目标 HTML 元素,将对其类进行操作
|
|
111
|
+
* @param oldClazz - 要替换的旧类名或类索引(基于数字时对应 classList 的索引位置)
|
|
112
|
+
* @param newClazz - 新的类名,用于替换旧的类
|
|
113
|
+
* @returns void
|
|
114
|
+
*/
|
|
115
|
+
function replaceClass(el, oldClazz, newClazz) {
|
|
116
|
+
let old = typeof oldClazz === "number" ? el.classList.item(oldClazz) : oldClazz;
|
|
117
|
+
if (old) el.classList.replace(old, newClazz);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 为节点添加事件处理
|
|
121
|
+
* @param {HTMLElement} element 添加事件的节点
|
|
122
|
+
* @param {string} listener 事件名称
|
|
123
|
+
* @param {function} fn 事件处理函数
|
|
124
|
+
* @param {boolean} option 是否是只运行一次的处理函数或者配置,其中 eventFlag 为 string,如果配置该项,则表明为委托事件
|
|
125
|
+
*/
|
|
126
|
+
function on(element, listener, fn, option) {
|
|
127
|
+
const addListener = (el) => {
|
|
128
|
+
if (typeof option === "object" && option?.eventFlag && el instanceof Element) el.setAttribute(option.eventFlag, "__stop__");
|
|
129
|
+
el.addEventListener(listener, fn, option);
|
|
130
|
+
};
|
|
131
|
+
if (element instanceof Node) addListener(element);
|
|
132
|
+
else iterate(element, addListener);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 移除指定元素的事件监听器。
|
|
136
|
+
* @param el - 要移除监听器的 HTML 元素。
|
|
137
|
+
* @param listener - 事件名称。
|
|
138
|
+
* @param fn - 要移除的事件监听器函数。
|
|
139
|
+
*/
|
|
140
|
+
function off(el, listener, fn, option) {
|
|
141
|
+
const removeListener = (target) => {
|
|
142
|
+
target.removeEventListener(listener, fn, option);
|
|
143
|
+
};
|
|
144
|
+
if (el instanceof Node) removeListener(el);
|
|
145
|
+
else iterate(el, removeListener);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 判断事件是否应该继续传递。
|
|
149
|
+
* 从事件目标开始向上遍历DOM树,检查每个节点上是否存在指定的属性。
|
|
150
|
+
* 如果找到该属性且其值不为'__stop__',则返回true,表示事件可以继续传递。
|
|
151
|
+
* 否则,返回false,表示事件应该停止传递。
|
|
152
|
+
* 通常用于事件委托时判断是否继续传递事件。
|
|
153
|
+
*
|
|
154
|
+
* @param e - 触发的事件对象
|
|
155
|
+
* @param eventFlag - 需要检查的属性名
|
|
156
|
+
* @param endRoot - 可选,如果传递该参数,则表示停止遍历的节点,如果未传递,则表示遍历到文档根节点为止
|
|
157
|
+
* @returns 包含三个元素的数组:[是否继续传递事件, 属性值, 当前检查的DOM节点]
|
|
158
|
+
*/
|
|
159
|
+
function shouldEventNext(e, eventFlag, endRoot) {
|
|
160
|
+
let target = e.target;
|
|
161
|
+
let flag = "";
|
|
162
|
+
do {
|
|
163
|
+
if (endRoot && endRoot.isSameNode(target) || target.tagName === "BODY") break;
|
|
164
|
+
if (target.getAttribute) flag = target.getAttribute(eventFlag) || "";
|
|
165
|
+
if (flag === "") target = target.parentNode;
|
|
166
|
+
if (!target) break;
|
|
167
|
+
} while (flag === "");
|
|
168
|
+
return [
|
|
169
|
+
flag !== "__stop__" && flag !== "",
|
|
170
|
+
flag,
|
|
171
|
+
target
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 设置或获取节点的 innerHTML 属性
|
|
176
|
+
* @param element
|
|
177
|
+
* @param htmlstr 可选,如果传递该参数,则表示设置;否则表示获取
|
|
178
|
+
* @returns
|
|
179
|
+
*/
|
|
180
|
+
function html(element, htmlstr) {
|
|
181
|
+
if (htmlstr == null) return element.innerHTML;
|
|
182
|
+
else {
|
|
183
|
+
element.innerHTML = htmlstr;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 设置或获取节点的 textContent 属性
|
|
189
|
+
* @param element
|
|
190
|
+
* @param textstr 可选,如果传递该参数,则表示设置;否则表示获取
|
|
191
|
+
* @returns
|
|
192
|
+
*/
|
|
193
|
+
function text(element, textstr) {
|
|
194
|
+
if (textstr == null) return element.textContent;
|
|
195
|
+
else {
|
|
196
|
+
element.textContent = textstr;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 节点列表遍历
|
|
202
|
+
* @param elems
|
|
203
|
+
* @param fn 遍历到节点时的回调,回调第一个参数为遍历到的节点,第2个参数为 index;如果回调函数返回 true,则会终止遍历(break)
|
|
204
|
+
*/
|
|
205
|
+
function iterate(elems, fn) {
|
|
206
|
+
for (let i = 0, len = elems.length; i < len; i++) if (fn(elems[i], i) === true) break;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 设置或获取节点 data-* 属性
|
|
210
|
+
* @param elem
|
|
211
|
+
* @param key data- 后面跟随的值
|
|
212
|
+
* @param value 如果传递该值表示获取;否则表示设置
|
|
213
|
+
* @returns
|
|
214
|
+
*/
|
|
215
|
+
function attr(elem, key, value) {
|
|
216
|
+
if (value != null) elem.setAttribute("data-" + key, value);
|
|
217
|
+
else return elem.getAttribute("data-" + key);
|
|
218
|
+
}
|
|
219
|
+
function getAttr(el, key, defaultValue) {
|
|
220
|
+
const value = el.getAttribute(key);
|
|
221
|
+
if (defaultValue == null) return value;
|
|
222
|
+
const valueType = typeof defaultValue;
|
|
223
|
+
if (value == null) return defaultValue;
|
|
224
|
+
if (valueType === "bigint" || valueType === "number") {
|
|
225
|
+
if (value === "") return defaultValue;
|
|
226
|
+
return Number(value);
|
|
227
|
+
}
|
|
228
|
+
if (valueType === "boolean") {
|
|
229
|
+
if (value === "" || value === "1" || value === "true" || value === key) return true;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (valueType === "object") {
|
|
233
|
+
if (value === "") return defaultValue;
|
|
234
|
+
return JSON.parse(value);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 获取指定节点的父节点
|
|
240
|
+
* @param el
|
|
241
|
+
* @returns
|
|
242
|
+
*/
|
|
243
|
+
function parent(el) {
|
|
244
|
+
return el.parentNode;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 获取隐藏节点的尺寸, 如果 parent 传空, 且 hideNode 为节点,则会通过修改原始样式方式计算
|
|
248
|
+
* @param {string | HTMLElement} hideNode - The node to hide.
|
|
249
|
+
* @param parent - 添加临时节点的父节点, 如果传递 null, 则通过修改原始样式方式计算,默认为: body.
|
|
250
|
+
* @returns The DOMRect of the element.
|
|
251
|
+
*/
|
|
252
|
+
function queryHideNodeSize(hideNode, parent = document.body) {
|
|
253
|
+
if (parent == null && typeof hideNode !== "string") {
|
|
254
|
+
const originalDisplay = hideNode.style.display;
|
|
255
|
+
const originalVisibility = hideNode.style.visibility;
|
|
256
|
+
const originalPosition = hideNode.style.position;
|
|
257
|
+
hideNode.style.position = "absolute";
|
|
258
|
+
hideNode.style.visibility = "hidden";
|
|
259
|
+
hideNode.style.display = "block";
|
|
260
|
+
const rect = hideNode.getBoundingClientRect();
|
|
261
|
+
hideNode.style.display = originalDisplay;
|
|
262
|
+
hideNode.style.visibility = originalVisibility;
|
|
263
|
+
hideNode.style.position = originalPosition;
|
|
264
|
+
return {
|
|
265
|
+
width: rect.width,
|
|
266
|
+
height: rect.height
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
let $tmp = document.createElement("div");
|
|
270
|
+
$tmp.style.cssText = "position:fixed;left:-1000px;top:-1000px;opacity:0;";
|
|
271
|
+
let $tmpInner = document.createElement("div");
|
|
272
|
+
$tmpInner.style.cssText = "position:relative;";
|
|
273
|
+
if (typeof hideNode === "string") $tmpInner.innerHTML = hideNode;
|
|
274
|
+
else $tmpInner.appendChild(hideNode.cloneNode(true));
|
|
275
|
+
$tmp.appendChild($tmpInner);
|
|
276
|
+
(parent || document.body).appendChild($tmp);
|
|
277
|
+
let rect = $tmpInner.children[0].getBoundingClientRect();
|
|
278
|
+
(parent || document.body).removeChild($tmp);
|
|
279
|
+
return {
|
|
280
|
+
width: rect.width,
|
|
281
|
+
height: rect.height
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 判断元素是否在父元素的可视区域内。
|
|
286
|
+
*
|
|
287
|
+
* @param el 要检查的元素
|
|
288
|
+
* @param parent 元素的父元素,默认为document.body
|
|
289
|
+
* @param direction 检查的方向,默认为"horizontal"
|
|
290
|
+
* @returns 如果元素在父元素的可视区域内,则返回true;否则返回false。
|
|
291
|
+
*/
|
|
292
|
+
function isVisible(el, parent = null, direction = "horizontal") {
|
|
293
|
+
if (parent == null) parent = el.offsetParent;
|
|
294
|
+
const containerRect = parent.getBoundingClientRect();
|
|
295
|
+
const elementRect = el.getBoundingClientRect();
|
|
296
|
+
let elStart = direction === "horizontal" ? elementRect.left : elementRect.top;
|
|
297
|
+
let elEnd = direction === "horizontal" ? elementRect.right : elementRect.bottom;
|
|
298
|
+
let containerStart = direction === "horizontal" ? containerRect.left : containerRect.top;
|
|
299
|
+
let containerEnd = direction === "horizontal" ? containerRect.right : containerRect.bottom;
|
|
300
|
+
return elStart >= containerStart && elEnd <= containerEnd;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 判断当前设备是否为移动设备。
|
|
304
|
+
*
|
|
305
|
+
* 本函数通过检查用户代理字符串和屏幕尺寸来判断设备是否为移动设备。
|
|
306
|
+
* 这对于需要根据设备类型进行不同布局或功能调整的应用非常有用。
|
|
307
|
+
*
|
|
308
|
+
* @returns {boolean} 如果设备是移动设备,则返回true;否则返回false。
|
|
309
|
+
*/
|
|
310
|
+
function isMobile() {
|
|
311
|
+
const isMobile = /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
312
|
+
const screenWidth = window.innerWidth;
|
|
313
|
+
const screenHeight = window.innerHeight;
|
|
314
|
+
return isMobile || screenWidth <= 800 || screenHeight <= 600;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 格式化类名,支持数组和对象两种形式。
|
|
318
|
+
* - 数组形式:数组中的每个元素代表一个类名,非空元素将被添加到结果字符串中。
|
|
319
|
+
* - 对象形式:对象的键代表类名,值为真(非空、非undefined、非null)时,键将被添加到结果字符串中。
|
|
320
|
+
* @param classObj - 类名对象或数组
|
|
321
|
+
* @returns 格式化后的类名字符串
|
|
322
|
+
*/
|
|
323
|
+
function formatClass(classObj) {
|
|
324
|
+
let classes = "";
|
|
325
|
+
if (Array.isArray(classObj)) for (let i = 0, len = classObj.length; i < len; i++) {
|
|
326
|
+
const item = classObj[i];
|
|
327
|
+
if (item) classes += `${item} `;
|
|
328
|
+
}
|
|
329
|
+
else if (typeof classObj === "string") classes = classObj;
|
|
330
|
+
else for (const key in classObj) if (classObj[key]) classes += `${key} `;
|
|
331
|
+
return classes.trim();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 将样式对象格式化为 CSS 样式字符串。
|
|
335
|
+
* @param styleObj - 样式对象,可以是字符串数组或键值对对象。
|
|
336
|
+
* @returns 格式化后的 CSS 样式字符串。
|
|
337
|
+
*/
|
|
338
|
+
function formatStyle(styleObj) {
|
|
339
|
+
let styleStr = "";
|
|
340
|
+
if (Array.isArray(styleObj)) for (let i = 0, len = styleObj.length; i < len; i++) {
|
|
341
|
+
const item = styleObj[i];
|
|
342
|
+
if (item) styleStr += `${item};`;
|
|
343
|
+
}
|
|
344
|
+
else if (typeof styleObj === "string") styleStr = styleObj;
|
|
345
|
+
else for (const key in styleObj) {
|
|
346
|
+
const value = styleObj[key];
|
|
347
|
+
if (value) styleStr += `${key}:${value};`;
|
|
348
|
+
}
|
|
349
|
+
return styleStr;
|
|
350
|
+
}
|
|
351
|
+
function toggleCssProperty(el, properties, method = "set") {
|
|
352
|
+
for (let i = 0, len = properties.length; i < len; i++) {
|
|
353
|
+
const rec = properties[i];
|
|
354
|
+
if (method === "set") el.style.setProperty(rec[0], rec[1]);
|
|
355
|
+
else el.style.removeProperty(rec[0]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 执行元素的过渡动画。
|
|
360
|
+
*
|
|
361
|
+
* 如果是名称类似于 Vue.js 的过渡动画,通过添加 `*-active`、`*-from|to` class 类名。
|
|
362
|
+
*
|
|
363
|
+
* @param el - 需要执行过渡动画的 HTML 元素。
|
|
364
|
+
* @param nameOrProperties - 过渡动画的名称或属性数组。可以是字符串表示的动画名称,或者是包含属性名称、初始值或目标值、持续时间。eg. [['opacity', '0.5']] | 'nt-opacity'
|
|
365
|
+
* @param dir - 过渡动画的方向,"leave" 表示离开,"enter" 表示进入。默认值为 "enter"。
|
|
366
|
+
* @param finish - 过渡动画结束时的回调函数。
|
|
367
|
+
*
|
|
368
|
+
* @example <caption>执行 `nt-opacity` 名称动画</caption>
|
|
369
|
+
* // css
|
|
370
|
+
* .nt-opacity-enter-active,
|
|
371
|
+
* .nt-opacity-leave-active {
|
|
372
|
+
* transition: opacity 0.3s ease;
|
|
373
|
+
* }
|
|
374
|
+
* .nt-opacity-enter-from,
|
|
375
|
+
* .nt-opacity-leave-to {
|
|
376
|
+
* opacity: 0;
|
|
377
|
+
* }
|
|
378
|
+
* // js
|
|
379
|
+
* transition($el, "nt-opacity", "enter");
|
|
380
|
+
*
|
|
381
|
+
* @example <caption>执行 `style` 属性动画</caption>
|
|
382
|
+
* transition($el, [["opacity", "0", "0.3s"]], "enter");
|
|
383
|
+
*
|
|
384
|
+
* @example <caption>动画结束后移除节点</caption>
|
|
385
|
+
* transition($el, [["opacity", "0", "0.3s"]], "leave", () => { $el.remove(); });
|
|
386
|
+
*/
|
|
387
|
+
function transition(el, nameOrProperties, dir = "enter", finish) {
|
|
388
|
+
const p = dir === "enter" ? "from" : "to";
|
|
389
|
+
let nameClass = "", activeClass = "";
|
|
390
|
+
/** 动画状态, -1 - 准备, 0 - 进行中, 1 - 完成 */
|
|
391
|
+
let status = -1;
|
|
392
|
+
const trans = [];
|
|
393
|
+
if (typeof nameOrProperties === "string") {
|
|
394
|
+
nameClass = `${nameOrProperties}-${dir}-${p}`;
|
|
395
|
+
activeClass = `${nameOrProperties}-${dir}-active`;
|
|
396
|
+
} else for (let i = 0, len = nameOrProperties.length; i < len; i++) {
|
|
397
|
+
const rec = nameOrProperties[i];
|
|
398
|
+
if (rec.length >= 3) trans.push(`${rec[0]} ${rec[2]}`);
|
|
399
|
+
}
|
|
400
|
+
status = 0;
|
|
401
|
+
if (dir === "enter") {
|
|
402
|
+
if (nameClass) {
|
|
403
|
+
el.classList.add(nameClass);
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
el.classList.add(activeClass);
|
|
406
|
+
requestAnimationFrame(() => {
|
|
407
|
+
el.classList.remove(nameClass);
|
|
408
|
+
});
|
|
409
|
+
}, 0);
|
|
410
|
+
} else {
|
|
411
|
+
toggleCssProperty(el, nameOrProperties, "set");
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
if (trans.length > 0) el.style.setProperty("transition", trans.join(", "));
|
|
414
|
+
requestAnimationFrame(() => {
|
|
415
|
+
toggleCssProperty(el, nameOrProperties, "remove");
|
|
416
|
+
});
|
|
417
|
+
}, 0);
|
|
418
|
+
}
|
|
419
|
+
} else if (nameClass) {
|
|
420
|
+
el.classList.add(activeClass);
|
|
421
|
+
requestAnimationFrame(() => {
|
|
422
|
+
el.classList.add(nameClass);
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
if (trans.length > 0) el.style.setProperty("transition", trans.join(", "));
|
|
426
|
+
requestAnimationFrame(() => {
|
|
427
|
+
toggleCssProperty(el, nameOrProperties, "set");
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
el.addEventListener("transitionend", () => {
|
|
431
|
+
if (status === 0) {
|
|
432
|
+
status = 1;
|
|
433
|
+
if (nameClass) {
|
|
434
|
+
el.classList.remove(activeClass);
|
|
435
|
+
requestAnimationFrame(() => {
|
|
436
|
+
el.classList.remove(nameClass);
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
if (trans) el.style.removeProperty("transition");
|
|
440
|
+
requestAnimationFrame(() => {
|
|
441
|
+
toggleCssProperty(el, nameOrProperties, "remove");
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (finish) finish();
|
|
445
|
+
}
|
|
446
|
+
}, { once: true });
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 计算光标位置
|
|
450
|
+
*
|
|
451
|
+
* 根据输入值的变化计算新的光标位置,主要用于输入框内容变化时的光标位置调整
|
|
452
|
+
*
|
|
453
|
+
* @param start - 光标起始位置,通常为: input.selectionStart
|
|
454
|
+
* @param end - 光标结束位置(通常与起始位置相同,用于选择范围),通常为: input.selectionEnd
|
|
455
|
+
* @param oldValue - 变化前的值
|
|
456
|
+
* @param newValue - 变化后的值
|
|
457
|
+
* @returns 新的光标位置对象,包含新的起始和结束位置
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* // 删除字符时,光标向前移动
|
|
461
|
+
* calcuteCursorPosition(2, 2, "abc", "ac"); // 返回 { start: 1, end: 1 }
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* // 添加字符时,光标向后移动
|
|
465
|
+
* calcuteCursorPosition(2, 2, "ac", "abc"); // 返回 { start: 3, end: 3 }
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* // 替换字符时,保持位置
|
|
469
|
+
* calcuteCursorPosition(2, 2, "abc", "axc"); // 返回 { start: 2, end: 2 }
|
|
470
|
+
*/
|
|
471
|
+
function calcuteCursorPosition(start, end, oldValue, newValue) {
|
|
472
|
+
let newStart = start;
|
|
473
|
+
let newEnd = end;
|
|
474
|
+
if (newValue.length < oldValue.length) {
|
|
475
|
+
newStart = Math.max(0, start - (oldValue.length - newValue.length));
|
|
476
|
+
newEnd = newStart;
|
|
477
|
+
} else if (newValue.length > oldValue.length) {
|
|
478
|
+
newStart = Math.min(newValue.length, start + (newValue.length - oldValue.length));
|
|
479
|
+
newEnd = newStart;
|
|
480
|
+
} else {
|
|
481
|
+
newStart = Math.min(newValue.length, start);
|
|
482
|
+
newEnd = newStart;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
start: newStart,
|
|
486
|
+
end: newEnd
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/copy.ts
|
|
491
|
+
/**
|
|
492
|
+
* 创建一个临时节点缓存待复制数据
|
|
493
|
+
* @param {String} value
|
|
494
|
+
* @return {HTMLElement}
|
|
495
|
+
*/
|
|
496
|
+
function createFakeElement(value) {
|
|
497
|
+
const fakeElement = document.createElement("textarea");
|
|
498
|
+
fakeElement.style.position = "fixed";
|
|
499
|
+
fakeElement.style.left = "-9999px";
|
|
500
|
+
fakeElement.style.top = "0";
|
|
501
|
+
fakeElement.style.opacity = "0";
|
|
502
|
+
fakeElement.style.pointerEvents = "none";
|
|
503
|
+
fakeElement.style.border = "0";
|
|
504
|
+
fakeElement.style.padding = "0";
|
|
505
|
+
fakeElement.style.margin = "0";
|
|
506
|
+
fakeElement.readOnly = true;
|
|
507
|
+
fakeElement.setAttribute("aria-hidden", "true");
|
|
508
|
+
fakeElement.value = value;
|
|
509
|
+
return fakeElement;
|
|
510
|
+
}
|
|
511
|
+
/** 通过执行 execCommand 来执行复制 */
|
|
512
|
+
function copyFromCommand(text) {
|
|
513
|
+
const fakeEl = createFakeElement(text);
|
|
514
|
+
document.body.append(fakeEl);
|
|
515
|
+
fakeEl.focus();
|
|
516
|
+
fakeEl.select();
|
|
517
|
+
fakeEl.setSelectionRange(0, fakeEl.value.length);
|
|
518
|
+
const res = document.execCommand("copy");
|
|
519
|
+
fakeEl.remove();
|
|
520
|
+
return Promise.resolve(res);
|
|
521
|
+
}
|
|
522
|
+
/** 使用 navigator.clipboard 复制 */
|
|
523
|
+
async function copyFromClipboard(text) {
|
|
524
|
+
const theClipboard = navigator.clipboard;
|
|
525
|
+
if (theClipboard != null) try {
|
|
526
|
+
theClipboard.writeText(text);
|
|
527
|
+
return true;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
/** 解析待复制的文本 */
|
|
534
|
+
function parseCopyText(source) {
|
|
535
|
+
let copyText = null;
|
|
536
|
+
let sourceEl = null;
|
|
537
|
+
if (typeof source === "string") {
|
|
538
|
+
if (source.startsWith("#") || source.startsWith(".")) {
|
|
539
|
+
sourceEl = document.querySelector(source);
|
|
540
|
+
if (sourceEl == null) copyText = source;
|
|
541
|
+
} else copyText = source;
|
|
542
|
+
}
|
|
543
|
+
if (source instanceof HTMLElement) sourceEl = source;
|
|
544
|
+
if (sourceEl != null) {
|
|
545
|
+
if (sourceEl.hasAttribute("data-copy-text")) copyText = sourceEl.getAttribute("data-copy-text");
|
|
546
|
+
else {
|
|
547
|
+
const tagName = sourceEl.tagName;
|
|
548
|
+
if (tagName === "INPUT" || tagName === "TEXTAREA") copyText = sourceEl.value;
|
|
549
|
+
else copyText = sourceEl.textContent;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return copyText;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 复制数据, 可以从多种类型的数据
|
|
556
|
+
* 1. 直接复制文本: await copy("待复制的文本")
|
|
557
|
+
* 2. 复制节点上的 data-copy-text:
|
|
558
|
+
* <button data-copy-text="这是待复制的文本">复制</button>
|
|
559
|
+
* await copy(e.target) // or await copy("#a") or await copy(document.querySelector('#a'))
|
|
560
|
+
* 3. 直接复制节点本身数据: await copy('#a')
|
|
561
|
+
* @param {string | HTMLElement} source 复制源, 从中解析待复制的数据
|
|
562
|
+
* @returns {Promise<boolean>} 是否复制成功
|
|
563
|
+
*/
|
|
564
|
+
async function copy(source) {
|
|
565
|
+
const copyText = parseCopyText(source);
|
|
566
|
+
if (copyText == null) return false;
|
|
567
|
+
if (!await copyFromClipboard(copyText)) return copyFromCommand(copyText);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/storage.ts
|
|
572
|
+
function getStorage(storage = "session") {
|
|
573
|
+
return storage === "session" ? sessionStorage : localStorage;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* 存储值到 Storage 中
|
|
577
|
+
* @param key 设置的 key
|
|
578
|
+
* @param value 设置的值
|
|
579
|
+
* @param [option.storage] session 或 local, 默认: session
|
|
580
|
+
* @param [option.expire] 数据有效期, 单位秒, 默认: -1 - 永久存储
|
|
581
|
+
*
|
|
582
|
+
* @example <caption>1. 存储到 SessionStorage</caption>
|
|
583
|
+
* set("key", "value");
|
|
584
|
+
*
|
|
585
|
+
* @example <caption>2. 存储到 LocalStorage</caption>
|
|
586
|
+
* set("key", "value", { storage: "local" });
|
|
587
|
+
*/
|
|
588
|
+
function set(key, value, option) {
|
|
589
|
+
const opts = {
|
|
590
|
+
expire: -1,
|
|
591
|
+
storage: "session",
|
|
592
|
+
...option
|
|
593
|
+
};
|
|
594
|
+
const saveData = JSON.stringify({
|
|
595
|
+
value,
|
|
596
|
+
time: Date.now(),
|
|
597
|
+
expire: opts.expire === -1 ? -1 : Math.floor(Date.now() / 1e3) + opts.expire
|
|
598
|
+
});
|
|
599
|
+
getStorage(opts.storage).setItem(key, saveData);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* 清空所有的缓存内容
|
|
603
|
+
* @param storage 待清空的缓存对象
|
|
604
|
+
*/
|
|
605
|
+
function clear(storage) {
|
|
606
|
+
getStorage(storage).clear();
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 删除存储到 Storage 中的数据
|
|
610
|
+
* @param key
|
|
611
|
+
* @param storage
|
|
612
|
+
*/
|
|
613
|
+
function remove(key, storage) {
|
|
614
|
+
getStorage(storage).removeItem(key);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 从 Storage 中取出数据
|
|
618
|
+
* @param key 保存时的 key
|
|
619
|
+
* @param defaultValue 没有数据时的默认值
|
|
620
|
+
* @param [option.delete] 是否在取出后,删除数据,默认:false - 取出后删除数据
|
|
621
|
+
* @param [option.storage] 使用的 Storage ,可以是 localStorage、sessionStorage, 默认: localStorage、sessionStorage
|
|
622
|
+
* @returns Storage 中 key 对应的数据
|
|
623
|
+
*/
|
|
624
|
+
function get(key, defaultValue, option) {
|
|
625
|
+
const opts = option || { delete: false };
|
|
626
|
+
const storage = getStorage(opts.storage);
|
|
627
|
+
let data = storage.getItem(key);
|
|
628
|
+
if (data == null) return defaultValue || null;
|
|
629
|
+
data = JSON.parse(data);
|
|
630
|
+
let d = data.value;
|
|
631
|
+
if (data.expire !== -1) {
|
|
632
|
+
if (Math.floor(Date.now() / 1e3) > data.expire) {
|
|
633
|
+
d = null;
|
|
634
|
+
storage.removeItem(key);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (opts.delete) storage.removeItem(key);
|
|
638
|
+
return d || defaultValue;
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/theme.ts
|
|
642
|
+
/** 获取当前系统的主题 */
|
|
643
|
+
function getSystemTheme() {
|
|
644
|
+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark";
|
|
645
|
+
else if (window.matchMedia("(prefers-color-scheme: light)").matches) return "light";
|
|
646
|
+
return "auto";
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* 初始化主题, 让网页能够适应系统主题, 同时根据缓存的主题切换主题
|
|
650
|
+
* @returns 当前应用的主题
|
|
651
|
+
*/
|
|
652
|
+
async function initTheme() {
|
|
653
|
+
let $themeStyle = document.getElementById("theme-style");
|
|
654
|
+
if ($themeStyle == null) {
|
|
655
|
+
$themeStyle = document.createElement("style");
|
|
656
|
+
$themeStyle.id = "theme-style";
|
|
657
|
+
$themeStyle.innerHTML = ":root{color-scheme:light dark;}html.light{color-scheme: light;}html.dark {color-scheme: dark;}";
|
|
658
|
+
document.head.appendChild($themeStyle);
|
|
659
|
+
}
|
|
660
|
+
return toggleTheme(localStorage.getItem("web-theme-appearance"));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 切换主题, 通常用于预览
|
|
664
|
+
* @param theme 切换的主题
|
|
665
|
+
* @param transition 是否使用过渡动画, 注意浏览器必须支持 document.startViewTransition, 默认: true
|
|
666
|
+
* @returns 切换后的主题
|
|
667
|
+
*/
|
|
668
|
+
async function toggleTheme(theme, transition = true) {
|
|
669
|
+
return new Promise((resolve) => {
|
|
670
|
+
const classList = document.documentElement.classList;
|
|
671
|
+
if (theme == null) theme = getSystemTheme();
|
|
672
|
+
function updateThemeClass() {
|
|
673
|
+
if (theme === "light") {
|
|
674
|
+
classList.add("light");
|
|
675
|
+
classList.remove("dark");
|
|
676
|
+
} else if (theme === "dark") {
|
|
677
|
+
classList.add("dark");
|
|
678
|
+
classList.remove("light");
|
|
679
|
+
} else classList.remove("light", "dark");
|
|
680
|
+
}
|
|
681
|
+
if (transition && document.startViewTransition) document.startViewTransition(() => {
|
|
682
|
+
updateThemeClass();
|
|
683
|
+
resolve(theme);
|
|
684
|
+
});
|
|
685
|
+
else {
|
|
686
|
+
updateThemeClass();
|
|
687
|
+
resolve(theme);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/** 获取当前主题 */
|
|
692
|
+
function getTheme() {
|
|
693
|
+
let theme = document.documentElement.className.match(/light|dark/);
|
|
694
|
+
if (theme != null) theme = theme[0];
|
|
695
|
+
if (theme == null) theme = localStorage.getItem("web-theme-appearance");
|
|
696
|
+
if (theme == null) theme = getSystemTheme();
|
|
697
|
+
return theme;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* 应用主题
|
|
701
|
+
* @param theme 待应用的主题
|
|
702
|
+
* @param cache 是否缓存应用的主题, 让应用下一次启动的时候, 可以应用主题, 默认: true
|
|
703
|
+
* @param transition 是否使用过渡动画, 注意浏览器必须支持 document.startViewTransition, 默认: true
|
|
704
|
+
* @returns 应用的主题
|
|
705
|
+
*/
|
|
706
|
+
async function applyTheme(theme, cache = true, transition = true) {
|
|
707
|
+
if (cache === true) localStorage.setItem("web-theme-appearance", theme == null ? "auto" : theme);
|
|
708
|
+
return toggleTheme(theme, transition);
|
|
709
|
+
}
|
|
710
|
+
/** 获取当前主题色 */
|
|
711
|
+
function getColorTheme(defaultValue) {
|
|
712
|
+
const root = document.documentElement;
|
|
713
|
+
const match = root.className.match(/color-([0-9a-fA-F]{6})/);
|
|
714
|
+
if (match == null) {
|
|
715
|
+
let color = getComputedStyle(root).getPropertyValue("--l-primary-color");
|
|
716
|
+
if (color === "") color = localStorage.getItem("web-theme-color");
|
|
717
|
+
return color ? color : defaultValue;
|
|
718
|
+
}
|
|
719
|
+
return `#${match[1]}`;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* 初始化主题色, 让网页能够适应系统主题色, 同时根据缓存的主题色切换主题色
|
|
723
|
+
* @returns 当前应用的主题色
|
|
724
|
+
*/
|
|
725
|
+
function initColorTheme() {
|
|
726
|
+
const color = localStorage.getItem("web-theme-color");
|
|
727
|
+
if (color != null) return toggleColorTheme(color);
|
|
728
|
+
return color;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 切换主题色, 通常用于预览
|
|
732
|
+
* @param color 待切换的主题色
|
|
733
|
+
* @returns 切换后的主题色
|
|
734
|
+
*/
|
|
735
|
+
async function toggleColorTheme(color) {
|
|
736
|
+
const vars = [`--l-primary-color: ${color};`, `--l-primary-color-dark1: ${adjust(color, 1, false)};`];
|
|
737
|
+
for (let i = 1; i <= 5; i++) vars.push(`--l-primary-color-light${i}: ${adjust(color, i)};`);
|
|
738
|
+
let $style = document.getElementById("color-theme-style");
|
|
739
|
+
if ($style == null) {
|
|
740
|
+
$style = document.createElement("style");
|
|
741
|
+
$style.id = "color-theme-style";
|
|
742
|
+
document.head.appendChild($style);
|
|
743
|
+
}
|
|
744
|
+
$style.innerHTML = `:root{${vars.join("")}}`;
|
|
745
|
+
return color;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* 应用主题色
|
|
749
|
+
* @param color 主题色
|
|
750
|
+
* @param cache 是否缓存主题色, 让应用下一次启动的时候, 可以应用主题色, 默认: true
|
|
751
|
+
* @returns 切换后的主题色
|
|
752
|
+
*/
|
|
753
|
+
function applyColorTheme(color, cache = true) {
|
|
754
|
+
if (cache === true) localStorage.setItem("web-theme-color", color);
|
|
755
|
+
return toggleColorTheme(color);
|
|
756
|
+
}
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/web.ts
|
|
759
|
+
/**
|
|
760
|
+
* 解析 Form 表单中的 input 元素的数据为 JSON 格式,key: input-name;value: input-value
|
|
761
|
+
* @param form {object} Form 节点对象
|
|
762
|
+
*/
|
|
763
|
+
const formJson = function(form) {
|
|
764
|
+
const value = {};
|
|
765
|
+
const elements = form.elements;
|
|
766
|
+
for (let i = 0; i < elements.length; i++) {
|
|
767
|
+
const item = elements[i];
|
|
768
|
+
if (!item.name) continue;
|
|
769
|
+
if (item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement) {
|
|
770
|
+
const val = item.value.trim();
|
|
771
|
+
if (!val) continue;
|
|
772
|
+
switch (item.dataset.type) {
|
|
773
|
+
case "number":
|
|
774
|
+
value[item.name] = Number(val);
|
|
775
|
+
break;
|
|
776
|
+
case "boolean":
|
|
777
|
+
value[item.name] = val === "true" || val === "1";
|
|
778
|
+
break;
|
|
779
|
+
default: value[item.name] = val;
|
|
780
|
+
}
|
|
781
|
+
} else if (item instanceof HTMLSelectElement) value[item.name] = item.options[item.selectedIndex]?.value ?? "";
|
|
782
|
+
}
|
|
783
|
+
return value;
|
|
784
|
+
};
|
|
785
|
+
/**
|
|
786
|
+
* 获取 url query 参数 (get 请求的参数)
|
|
787
|
+
* @param search 如果是 React 应用就需要传递 useLocation().search
|
|
788
|
+
* @returns
|
|
789
|
+
*/
|
|
790
|
+
function query(search) {
|
|
791
|
+
const searchStr = search ?? location.search;
|
|
792
|
+
const searchParams = new URLSearchParams(searchStr);
|
|
793
|
+
const result = {};
|
|
794
|
+
for (const key of new Set(searchParams.keys())) {
|
|
795
|
+
const values = searchParams.getAll(key);
|
|
796
|
+
result[key] = values.length > 1 ? values : values[0];
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* 函数节流 - 每隔单位时间,只执行一次
|
|
802
|
+
* @param cb 待节流的函数
|
|
803
|
+
* @param wait 间隔时间
|
|
804
|
+
* @returns
|
|
805
|
+
*/
|
|
806
|
+
function throttle(fn, wait = 500) {
|
|
807
|
+
let last = 0;
|
|
808
|
+
let timer = null;
|
|
809
|
+
const throttled = function(...args) {
|
|
810
|
+
const now = Date.now();
|
|
811
|
+
const remaining = wait - (now - last);
|
|
812
|
+
if (remaining <= 0 || remaining > wait) {
|
|
813
|
+
if (timer) {
|
|
814
|
+
clearTimeout(timer);
|
|
815
|
+
timer = null;
|
|
816
|
+
}
|
|
817
|
+
last = now;
|
|
818
|
+
fn.apply(this, args);
|
|
819
|
+
} else if (!timer) timer = setTimeout(() => {
|
|
820
|
+
last = Date.now();
|
|
821
|
+
timer = null;
|
|
822
|
+
fn.apply(this, args);
|
|
823
|
+
}, remaining);
|
|
824
|
+
};
|
|
825
|
+
throttled.cancel = function() {
|
|
826
|
+
if (timer) {
|
|
827
|
+
clearTimeout(timer);
|
|
828
|
+
timer = null;
|
|
829
|
+
}
|
|
830
|
+
last = 0;
|
|
831
|
+
};
|
|
832
|
+
return throttled;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* 函数防抖 - 当重复触发某一个行为(事件时),只执行最后一次触发
|
|
836
|
+
* @param fn 防抖函数
|
|
837
|
+
* @param interval 间隔时间段
|
|
838
|
+
* @returns
|
|
839
|
+
*/
|
|
840
|
+
function debounce(fn, interval = 500) {
|
|
841
|
+
let timer = null;
|
|
842
|
+
const debounced = function(...args) {
|
|
843
|
+
if (timer) clearTimeout(timer);
|
|
844
|
+
timer = setTimeout(() => {
|
|
845
|
+
fn.apply(this, args);
|
|
846
|
+
timer = null;
|
|
847
|
+
}, interval);
|
|
848
|
+
};
|
|
849
|
+
debounced.cancel = function() {
|
|
850
|
+
if (timer) {
|
|
851
|
+
clearTimeout(timer);
|
|
852
|
+
timer = null;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
return debounced;
|
|
856
|
+
}
|
|
857
|
+
//#endregion
|
|
858
|
+
export { $, $$, $one, addClass, applyColorTheme, applyTheme, attr, calcuteCursorPosition, clear, copy, create, debounce, elem, formJson, formatClass, formatStyle, get, getAttr, getColorTheme, getSystemTheme, getTheme, hasClass, html, initColorTheme, initTheme, isMobile, isVisible, iterate, off, on, parent, query, queryHideNodeSize, remove, removeClass, replaceClass, set, shouldEventNext, text, throttle, toggleClass, toggleColorTheme, toggleTheme, transition };
|