sculp-js 1.13.1 → 1.13.3-beta.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/README.md +10 -11
- package/dist/cjs/_virtual/_commonjsHelpers.js +13 -0
- package/{lib → dist}/cjs/array.js +1 -1
- package/{lib → dist}/cjs/async.js +1 -1
- package/{lib/cjs/we-decode.js → dist/cjs/base64.js} +53 -44
- package/{lib → dist}/cjs/clipboard.js +4 -2
- package/{lib → dist}/cjs/cloneDeep.js +1 -1
- package/{lib → dist}/cjs/cookie.js +1 -1
- package/{lib → dist}/cjs/date.js +1 -1
- package/{lib → dist}/cjs/dom.js +1 -1
- package/{lib → dist}/cjs/download.js +4 -2
- package/dist/cjs/easing.js +75 -0
- package/{lib → dist}/cjs/file.js +7 -3
- package/{lib → dist}/cjs/func.js +1 -1
- package/{lib → dist}/cjs/index.js +11 -8
- package/{lib → dist}/cjs/math.js +1 -1
- package/dist/cjs/node_modules/bezier-easing/src/index.js +123 -0
- package/{lib → dist}/cjs/number.js +1 -1
- package/{lib → dist}/cjs/object.js +1 -1
- package/{lib → dist}/cjs/path.js +1 -1
- package/{lib → dist}/cjs/qs.js +1 -1
- package/{lib → dist}/cjs/random.js +1 -1
- package/{lib → dist}/cjs/string.js +1 -1
- package/{lib → dist}/cjs/tooltip.js +5 -2
- package/{lib → dist}/cjs/tree.js +9 -23
- package/{lib → dist}/cjs/type.js +8 -1
- package/{lib → dist}/cjs/unique.js +1 -1
- package/{lib → dist}/cjs/url.js +1 -1
- package/{lib → dist}/cjs/validator.js +1 -1
- package/{lib → dist}/cjs/variable.js +1 -1
- package/{lib → dist}/cjs/watermark.js +4 -2
- package/{lib/es → dist/esm}/array.js +1 -1
- package/{lib/es → dist/esm}/async.js +1 -1
- package/{lib/es/we-decode.js → dist/esm/base64.js} +52 -45
- package/{lib/es → dist/esm}/clipboard.js +4 -2
- package/{lib/es → dist/esm}/cloneDeep.js +1 -1
- package/{lib/es → dist/esm}/cookie.js +1 -1
- package/{lib/es → dist/esm}/date.js +1 -1
- package/{lib/es → dist/esm}/dom.js +1 -1
- package/{lib/es → dist/esm}/download.js +4 -2
- package/{lib/es → dist/esm}/easing.js +34 -2
- package/{lib/es → dist/esm}/file.js +7 -3
- package/{lib/es → dist/esm}/func.js +1 -1
- package/{lib/es → dist/esm}/index.js +4 -5
- package/{lib/es → dist/esm}/math.js +1 -1
- package/{lib/es → dist/esm}/number.js +1 -1
- package/{lib/es → dist/esm}/object.js +1 -1
- package/{lib/es → dist/esm}/path.js +1 -1
- package/{lib/es → dist/esm}/qs.js +1 -1
- package/{lib/es → dist/esm}/random.js +1 -1
- package/{lib/es → dist/esm}/string.js +1 -1
- package/{lib/es → dist/esm}/tooltip.js +5 -2
- package/{lib/es → dist/esm}/tree.js +10 -24
- package/{lib/es → dist/esm}/type.js +8 -2
- package/{lib/es → dist/esm}/unique.js +1 -1
- package/{lib/es → dist/esm}/url.js +1 -1
- package/{lib/es → dist/esm}/validator.js +1 -1
- package/{lib/es → dist/esm}/variable.js +1 -1
- package/{lib/es → dist/esm}/watermark.js +4 -2
- package/dist/types/array.d.ts +50 -0
- package/dist/types/array.d.ts.map +1 -0
- package/dist/types/async.d.ts +37 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/base64.d.ts +25 -0
- package/dist/types/base64.d.ts.map +1 -0
- package/dist/types/clipboard.d.ts +20 -0
- package/dist/types/clipboard.d.ts.map +1 -0
- package/dist/types/cloneDeep.d.ts +12 -0
- package/dist/types/cloneDeep.d.ts.map +1 -0
- package/dist/types/cookie.d.ts +19 -0
- package/dist/types/cookie.d.ts.map +1 -0
- package/dist/types/core-index.d.ts +17 -0
- package/dist/types/core-index.d.ts.map +1 -0
- package/dist/types/date.d.ts +74 -0
- package/dist/types/date.d.ts.map +1 -0
- package/dist/types/dom.d.ts +74 -0
- package/dist/types/dom.d.ts.map +1 -0
- package/dist/types/download.d.ts +47 -0
- package/dist/types/download.d.ts.map +1 -0
- package/dist/types/easing.d.ts +32 -0
- package/dist/types/easing.d.ts.map +1 -0
- package/dist/types/file.d.ts +44 -0
- package/dist/types/file.d.ts.map +1 -0
- package/dist/types/func.d.ts +50 -0
- package/dist/types/func.d.ts.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3278 -0
- package/dist/types/math.d.ts +36 -0
- package/dist/types/math.d.ts.map +1 -0
- package/dist/types/number.d.ts +49 -0
- package/dist/types/number.d.ts.map +1 -0
- package/dist/types/object.d.ts +70 -0
- package/dist/types/object.d.ts.map +1 -0
- package/dist/types/path.d.ts +14 -0
- package/dist/types/path.d.ts.map +1 -0
- package/dist/types/qs.d.ts +22 -0
- package/dist/types/qs.d.ts.map +1 -0
- package/dist/types/random.d.ts +27 -0
- package/dist/types/random.d.ts.map +1 -0
- package/dist/types/string.d.ts +67 -0
- package/dist/types/string.d.ts.map +1 -0
- package/dist/types/tooltip.d.ts +36 -0
- package/dist/types/tooltip.d.ts.map +1 -0
- package/dist/types/tree.d.ts +99 -0
- package/dist/types/tree.d.ts.map +1 -0
- package/dist/types/type.d.ts +128 -0
- package/dist/types/type.d.ts.map +1 -0
- package/dist/types/unique.d.ts +21 -0
- package/dist/types/unique.d.ts.map +1 -0
- package/dist/types/url.d.ts +46 -0
- package/dist/types/url.d.ts.map +1 -0
- package/dist/types/validator.d.ts +67 -0
- package/dist/types/validator.d.ts.map +1 -0
- package/dist/types/variable.d.ts +71 -0
- package/dist/types/variable.d.ts.map +1 -0
- package/dist/types/watermark.d.ts +19 -0
- package/dist/types/watermark.d.ts.map +1 -0
- package/dist/umd/index.min.js +6 -0
- package/package.json +234 -17
- package/lib/cjs/base64.js +0 -62
- package/lib/cjs/easing.js +0 -40
- package/lib/cjs/isEqual.js +0 -133
- package/lib/es/base64.js +0 -59
- package/lib/es/isEqual.js +0 -131
- package/lib/index.d.ts +0 -1214
- package/lib/umd/index.js +0 -6
|
@@ -0,0 +1,3278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 遍历数组,返回 false 中断遍历(支持continue和break操作)
|
|
3
|
+
*
|
|
4
|
+
* @param {ArrayLike<V>} array
|
|
5
|
+
* @param {(val: V, idx: number) => any} iterator 迭代函数, 返回值为true时continue, 返回值为false时break
|
|
6
|
+
* @param {boolean} reverse 是否倒序
|
|
7
|
+
* @returns {*}
|
|
8
|
+
*/
|
|
9
|
+
function arrayEach(array, iterator, reverse = false) {
|
|
10
|
+
if (reverse) {
|
|
11
|
+
for (let idx = array.length - 1; idx >= 0; idx--) {
|
|
12
|
+
const re = iterator(array[idx], idx, array);
|
|
13
|
+
if (re === false)
|
|
14
|
+
break;
|
|
15
|
+
else if (re === true)
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
for (let idx = 0, len = array.length; idx < len; idx++) {
|
|
21
|
+
const re = iterator(array[idx], idx, array);
|
|
22
|
+
if (re === false)
|
|
23
|
+
break;
|
|
24
|
+
else if (re === true)
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
array = null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 异步遍历数组,返回 false 中断遍历
|
|
33
|
+
* @param {ArrayLike<V>} array 数组
|
|
34
|
+
* @param {(val: V, idx: number) => Promise<any>} iterator 支持Promise类型的回调函数
|
|
35
|
+
* @param {boolean} reverse 是否反向遍历
|
|
36
|
+
* @example
|
|
37
|
+
* 使用范例如下:
|
|
38
|
+
* const start = async () => {
|
|
39
|
+
* await arrayEachAsync(result, async (item) => {
|
|
40
|
+
* await request(item);
|
|
41
|
+
* count++;
|
|
42
|
+
* })
|
|
43
|
+
* console.log('发送次数', count);
|
|
44
|
+
* }
|
|
45
|
+
|
|
46
|
+
* for await...of 使用范例如下
|
|
47
|
+
* const loadImages = async (images) => {
|
|
48
|
+
* for await (const item of images) {
|
|
49
|
+
* await request(item);
|
|
50
|
+
* count++;
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
async function arrayEachAsync(array, iterator, reverse = false) {
|
|
55
|
+
if (reverse) {
|
|
56
|
+
for (let idx = array.length - 1; idx >= 0; idx--) {
|
|
57
|
+
if ((await iterator(array[idx], idx)) === false)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
for (let idx = 0, len = array.length; idx < len; idx++) {
|
|
63
|
+
if ((await iterator(array[idx], idx)) === false)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 插入到目标位置之前
|
|
70
|
+
* @param {AnyArray} array
|
|
71
|
+
* @param {number} start
|
|
72
|
+
* @param {number} to
|
|
73
|
+
* @returns {*}
|
|
74
|
+
*/
|
|
75
|
+
function arrayInsertBefore(array, start, to) {
|
|
76
|
+
if (start === to || start + 1 === to)
|
|
77
|
+
return;
|
|
78
|
+
const [source] = array.splice(start, 1);
|
|
79
|
+
const insertIndex = to < start ? to : to - 1;
|
|
80
|
+
array.splice(insertIndex, 0, source);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 数组删除指定项目
|
|
84
|
+
* @param {V[]} array
|
|
85
|
+
* @param {(val: V, idx: number) => boolean} expect
|
|
86
|
+
* @returns {V[]}
|
|
87
|
+
*/
|
|
88
|
+
function arrayRemove(array, expect) {
|
|
89
|
+
const indexes = [];
|
|
90
|
+
// 这里重命名一下:是为了杜绝 jest 里的 expect 与之产生检查错误
|
|
91
|
+
// eslint 的 jest 语法检查是遇到 expect 函数就以为是单元测试
|
|
92
|
+
const _expect = expect;
|
|
93
|
+
arrayEach(array, (val, idx) => {
|
|
94
|
+
if (_expect(val, idx))
|
|
95
|
+
indexes.push(idx);
|
|
96
|
+
});
|
|
97
|
+
indexes.forEach((val, idx) => {
|
|
98
|
+
array.splice(val - idx, 1);
|
|
99
|
+
});
|
|
100
|
+
return array;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getDefaultExportFromCjs (x) {
|
|
104
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* https://github.com/gre/bezier-easing
|
|
109
|
+
* BezierEasing - use bezier curve for transition easing function
|
|
110
|
+
* by Gaëtan Renaudeau 2014 - 2015 – MIT License
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
// These values are established by empiricism with tests (tradeoff: performance VS precision)
|
|
114
|
+
var NEWTON_ITERATIONS = 4;
|
|
115
|
+
var NEWTON_MIN_SLOPE = 0.001;
|
|
116
|
+
var SUBDIVISION_PRECISION = 0.0000001;
|
|
117
|
+
var SUBDIVISION_MAX_ITERATIONS = 10;
|
|
118
|
+
|
|
119
|
+
var kSplineTableSize = 11;
|
|
120
|
+
var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
|
|
121
|
+
|
|
122
|
+
var float32ArraySupported = typeof Float32Array === 'function';
|
|
123
|
+
|
|
124
|
+
function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
|
|
125
|
+
function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
|
|
126
|
+
function C (aA1) { return 3.0 * aA1; }
|
|
127
|
+
|
|
128
|
+
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
|
129
|
+
function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; }
|
|
130
|
+
|
|
131
|
+
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
|
132
|
+
function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); }
|
|
133
|
+
|
|
134
|
+
function binarySubdivide (aX, aA, aB, mX1, mX2) {
|
|
135
|
+
var currentX, currentT, i = 0;
|
|
136
|
+
do {
|
|
137
|
+
currentT = aA + (aB - aA) / 2.0;
|
|
138
|
+
currentX = calcBezier(currentT, mX1, mX2) - aX;
|
|
139
|
+
if (currentX > 0.0) {
|
|
140
|
+
aB = currentT;
|
|
141
|
+
} else {
|
|
142
|
+
aA = currentT;
|
|
143
|
+
}
|
|
144
|
+
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
|
|
145
|
+
return currentT;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) {
|
|
149
|
+
for (var i = 0; i < NEWTON_ITERATIONS; ++i) {
|
|
150
|
+
var currentSlope = getSlope(aGuessT, mX1, mX2);
|
|
151
|
+
if (currentSlope === 0.0) {
|
|
152
|
+
return aGuessT;
|
|
153
|
+
}
|
|
154
|
+
var currentX = calcBezier(aGuessT, mX1, mX2) - aX;
|
|
155
|
+
aGuessT -= currentX / currentSlope;
|
|
156
|
+
}
|
|
157
|
+
return aGuessT;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function LinearEasing (x) {
|
|
161
|
+
return x;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var src = function bezier (mX1, mY1, mX2, mY2) {
|
|
165
|
+
if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) {
|
|
166
|
+
throw new Error('bezier x values must be in [0, 1] range');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (mX1 === mY1 && mX2 === mY2) {
|
|
170
|
+
return LinearEasing;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Precompute samples table
|
|
174
|
+
var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
|
|
175
|
+
for (var i = 0; i < kSplineTableSize; ++i) {
|
|
176
|
+
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getTForX (aX) {
|
|
180
|
+
var intervalStart = 0.0;
|
|
181
|
+
var currentSample = 1;
|
|
182
|
+
var lastSample = kSplineTableSize - 1;
|
|
183
|
+
|
|
184
|
+
for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
|
|
185
|
+
intervalStart += kSampleStepSize;
|
|
186
|
+
}
|
|
187
|
+
--currentSample;
|
|
188
|
+
|
|
189
|
+
// Interpolate to provide an initial guess for t
|
|
190
|
+
var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
|
|
191
|
+
var guessForT = intervalStart + dist * kSampleStepSize;
|
|
192
|
+
|
|
193
|
+
var initialSlope = getSlope(guessForT, mX1, mX2);
|
|
194
|
+
if (initialSlope >= NEWTON_MIN_SLOPE) {
|
|
195
|
+
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
|
|
196
|
+
} else if (initialSlope === 0.0) {
|
|
197
|
+
return guessForT;
|
|
198
|
+
} else {
|
|
199
|
+
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return function BezierEasing (x) {
|
|
204
|
+
// Because JavaScript number are imprecise, we should guarantee the extremes are right.
|
|
205
|
+
if (x === 0) {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (x === 1) {
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
return calcBezier(getTForX(x), mY1, mY2);
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
var bezier = /*@__PURE__*/getDefaultExportFromCjs(src);
|
|
216
|
+
|
|
217
|
+
// 常用类型定义
|
|
218
|
+
const { toString, hasOwnProperty, propertyIsEnumerable } = Object.prototype;
|
|
219
|
+
/**
|
|
220
|
+
* 判断对象内是否有该静态属性
|
|
221
|
+
* @param {object} obj
|
|
222
|
+
* @param {string} key
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
function objectHas(obj, key) {
|
|
226
|
+
return hasOwnProperty.call(obj, key);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 判断一个对象是否为类数组
|
|
230
|
+
*
|
|
231
|
+
* @param any
|
|
232
|
+
* @returns {boolean}
|
|
233
|
+
*/
|
|
234
|
+
function arrayLike(any) {
|
|
235
|
+
if (isArray(any))
|
|
236
|
+
return true;
|
|
237
|
+
if (isString(any))
|
|
238
|
+
return true;
|
|
239
|
+
if (!isObject(any))
|
|
240
|
+
return false;
|
|
241
|
+
return objectHas(any, 'length');
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 判断任意值的数据类型,检查非对象时不如typeof、instanceof的性能高
|
|
245
|
+
*
|
|
246
|
+
* 当检查类对象时是不可靠的,对象可以通过定义 Symbol.toStringTag 属性来更改检查结果
|
|
247
|
+
*
|
|
248
|
+
* 详见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
|
|
249
|
+
* @param {unknown} any
|
|
250
|
+
* @returns
|
|
251
|
+
*/
|
|
252
|
+
function typeIs(any) {
|
|
253
|
+
return toString.call(any).slice(8, -1);
|
|
254
|
+
}
|
|
255
|
+
// 基本数据类型判断
|
|
256
|
+
const isString = (any) => typeof any === 'string';
|
|
257
|
+
const isBoolean = (any) => typeof any === 'boolean';
|
|
258
|
+
const isSymbol = (any) => typeof any === 'symbol';
|
|
259
|
+
const isBigInt = (any) => typeof any === 'bigint';
|
|
260
|
+
const isNumber = (any) => typeof any === 'number' && !Number.isNaN(any);
|
|
261
|
+
const isUndefined = (any) => typeof any === 'undefined';
|
|
262
|
+
const isNull = (any) => any === null;
|
|
263
|
+
const isPrimitive = (any) => any === null || typeof any !== 'object';
|
|
264
|
+
function isNullOrUnDef(val) {
|
|
265
|
+
return isUndefined(val) || isNull(val);
|
|
266
|
+
}
|
|
267
|
+
// 复合数据类型判断
|
|
268
|
+
const isObject = (any) => typeIs(any) === 'Object';
|
|
269
|
+
const isArray = (any) => Array.isArray(any);
|
|
270
|
+
/**
|
|
271
|
+
* 判断是否为函数
|
|
272
|
+
* @param {unknown} any
|
|
273
|
+
* @returns {boolean}
|
|
274
|
+
*/
|
|
275
|
+
const isFunction = (any) => typeof any === 'function';
|
|
276
|
+
// 对象类型判断
|
|
277
|
+
const isNaN = (any) => Number.isNaN(any);
|
|
278
|
+
const isDate = (any) => typeIs(any) === 'Date';
|
|
279
|
+
const isError = (any) => typeIs(any) === 'Error';
|
|
280
|
+
const isRegExp = (any) => typeIs(any) === 'RegExp';
|
|
281
|
+
/**
|
|
282
|
+
* 判断一个字符串是否为有效的 JSON, 若有效则返回有效的JSON对象,否则false
|
|
283
|
+
* @param {string} str
|
|
284
|
+
* @returns {Object | boolean}
|
|
285
|
+
*/
|
|
286
|
+
function isJsonString(str) {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(str);
|
|
289
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : false;
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Checks if `value` is an empty object, collection, map, or set.
|
|
297
|
+
*
|
|
298
|
+
* Objects are considered empty if they have no own enumerable string keyed
|
|
299
|
+
* properties.
|
|
300
|
+
*
|
|
301
|
+
* Array-like values such as `arguments` objects, arrays, buffers, strings, or
|
|
302
|
+
* jQuery-like collections are considered empty if they have a `length` of `0`.
|
|
303
|
+
* Similarly, maps and sets are considered empty if they have a `size` of `0`.
|
|
304
|
+
*
|
|
305
|
+
* @param {*} value The value to check.
|
|
306
|
+
* @returns {boolean} Returns `true` if `value` is empty, else `false`.
|
|
307
|
+
* @example
|
|
308
|
+
*
|
|
309
|
+
* isEmpty(null);
|
|
310
|
+
* // => true
|
|
311
|
+
*
|
|
312
|
+
* isEmpty(true);
|
|
313
|
+
* // => true
|
|
314
|
+
*
|
|
315
|
+
* isEmpty(1);
|
|
316
|
+
* // => true
|
|
317
|
+
*
|
|
318
|
+
* isEmpty([1, 2, 3]);
|
|
319
|
+
* // => false
|
|
320
|
+
*
|
|
321
|
+
* isEmpty({ 'a': 1 });
|
|
322
|
+
* // => false
|
|
323
|
+
*/
|
|
324
|
+
function isEmpty(value) {
|
|
325
|
+
if (isNullOrUnDef(value) || Number.isNaN(value)) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
const tag = typeIs(value);
|
|
329
|
+
if (arrayLike(value) || 'Arguments' === tag) {
|
|
330
|
+
return !value.length;
|
|
331
|
+
}
|
|
332
|
+
if ('Set' === tag || 'Map' === tag) {
|
|
333
|
+
return !value.size;
|
|
334
|
+
}
|
|
335
|
+
return !Object.keys(value).length;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Checks if `value` is an NodeList object
|
|
339
|
+
*/
|
|
340
|
+
function isNodeList(value) {
|
|
341
|
+
return isUndefined(NodeList) ? false : NodeList.prototype.isPrototypeOf(value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// @ref https://cubic-bezier.com/
|
|
345
|
+
const easingDefines = {
|
|
346
|
+
linear: [0, 0, 1, 1],
|
|
347
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
348
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
349
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
350
|
+
'ease-in-out': [0.42, 0, 0.58, 1]
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* 设置缓存定义
|
|
354
|
+
* @param {string} name
|
|
355
|
+
* @param {EasingDefine} define
|
|
356
|
+
*/
|
|
357
|
+
function setEasing(name, define) {
|
|
358
|
+
easingDefines[name] = define;
|
|
359
|
+
}
|
|
360
|
+
function getEasing(name) {
|
|
361
|
+
if (name)
|
|
362
|
+
return easingDefines[name];
|
|
363
|
+
return easingDefines;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 缓冲函数化,用于 js 计算缓冲进度
|
|
367
|
+
* @param {EasingNameOrDefine} [name=linear]
|
|
368
|
+
* @returns {EasingFunction}
|
|
369
|
+
*/
|
|
370
|
+
function easingFunctional(name) {
|
|
371
|
+
let fn;
|
|
372
|
+
if (isArray(name)) {
|
|
373
|
+
fn = bezier(...name);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const define = easingDefines[name];
|
|
377
|
+
if (!define) {
|
|
378
|
+
throw new Error(`${name} 缓冲函数未定义`);
|
|
379
|
+
}
|
|
380
|
+
fn = bezier(...define);
|
|
381
|
+
}
|
|
382
|
+
return (input) => fn(Math.max(0, Math.min(input, 1)));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 缓冲字符化,用于 css 设置缓冲属性
|
|
386
|
+
* @param {EasingNameOrDefine} name
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
function easingStringify(name) {
|
|
390
|
+
let bezierDefine;
|
|
391
|
+
if (isArray(name)) {
|
|
392
|
+
bezierDefine = name;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const define = easingDefines[name];
|
|
396
|
+
if (!define) {
|
|
397
|
+
throw new Error(`${name} 缓冲函数未定义`);
|
|
398
|
+
}
|
|
399
|
+
bezierDefine = define;
|
|
400
|
+
}
|
|
401
|
+
return `cubic-bezier(${bezierDefine.join(',')})`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 判断对象是否为纯对象
|
|
406
|
+
* @param {object} obj
|
|
407
|
+
* @returns {boolean}
|
|
408
|
+
*/
|
|
409
|
+
const isPlainObject = (obj) => {
|
|
410
|
+
if (!isObject(obj))
|
|
411
|
+
return false;
|
|
412
|
+
const proto = Object.getPrototypeOf(obj);
|
|
413
|
+
// 对象无原型
|
|
414
|
+
if (!proto)
|
|
415
|
+
return true;
|
|
416
|
+
// 是否对象直接实例
|
|
417
|
+
return proto === Object.prototype;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* 遍历对象,返回 false 中断遍历
|
|
421
|
+
* @param {O} obj
|
|
422
|
+
* @param {(val: O[keyof O], key: keyof O) => (boolean | void)} iterator
|
|
423
|
+
*/
|
|
424
|
+
function objectEach(obj, iterator) {
|
|
425
|
+
for (const key in obj) {
|
|
426
|
+
if (!objectHas(obj, key))
|
|
427
|
+
continue;
|
|
428
|
+
if (iterator(obj[key], key) === false)
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
// @ts-ignore
|
|
432
|
+
obj = null;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* 异步遍历对象,返回 false 中断遍历
|
|
436
|
+
* @param {O} obj
|
|
437
|
+
* @param {(val: O[keyof O], key: keyof O) => (boolean | void)} iterator
|
|
438
|
+
*/
|
|
439
|
+
async function objectEachAsync(obj, iterator) {
|
|
440
|
+
for (const key in obj) {
|
|
441
|
+
if (!objectHas(obj, key))
|
|
442
|
+
continue;
|
|
443
|
+
if ((await iterator(obj[key], key)) === false)
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 对象映射
|
|
449
|
+
* @param {O} obj
|
|
450
|
+
* @param {(val: O[keyof O], key: Extract<keyof O, string>) => any} iterator
|
|
451
|
+
* @returns {Record<Extract<keyof O, string>, T>}
|
|
452
|
+
*/
|
|
453
|
+
function objectMap(obj, iterator) {
|
|
454
|
+
const obj2 = {};
|
|
455
|
+
for (const key in obj) {
|
|
456
|
+
if (!objectHas(obj, key))
|
|
457
|
+
continue;
|
|
458
|
+
obj2[key] = iterator(obj[key], key);
|
|
459
|
+
}
|
|
460
|
+
// @ts-ignore
|
|
461
|
+
obj = null;
|
|
462
|
+
return obj2;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 对象提取
|
|
466
|
+
* @param {O} obj
|
|
467
|
+
* @param {K} keys
|
|
468
|
+
* @returns {Pick<O, ArrayElements<K>>}
|
|
469
|
+
*/
|
|
470
|
+
function objectPick(obj, keys) {
|
|
471
|
+
const obj2 = {};
|
|
472
|
+
objectEach(obj, (v, k) => {
|
|
473
|
+
if (keys.includes(k)) {
|
|
474
|
+
// @ts-ignore
|
|
475
|
+
obj2[k] = v;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
// @ts-ignore
|
|
479
|
+
obj = null;
|
|
480
|
+
return obj2;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* 对象去除
|
|
484
|
+
* @param {O} obj
|
|
485
|
+
* @param {K} keys
|
|
486
|
+
* @returns {Pick<O, ArrayElements<K>>}
|
|
487
|
+
*/
|
|
488
|
+
function objectOmit(obj, keys) {
|
|
489
|
+
const obj2 = {};
|
|
490
|
+
objectEach(obj, (v, k) => {
|
|
491
|
+
if (!keys.includes(k)) {
|
|
492
|
+
// @ts-ignore
|
|
493
|
+
obj2[k] = v;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// @ts-ignore
|
|
497
|
+
obj = null;
|
|
498
|
+
return obj2;
|
|
499
|
+
}
|
|
500
|
+
const merge = (map, source, target) => {
|
|
501
|
+
if (isUndefined(target))
|
|
502
|
+
return source;
|
|
503
|
+
const sourceType = typeIs(source);
|
|
504
|
+
const targetType = typeIs(target);
|
|
505
|
+
if (sourceType !== targetType) {
|
|
506
|
+
if (isArray(target))
|
|
507
|
+
return merge(map, [], target);
|
|
508
|
+
if (isObject(target))
|
|
509
|
+
return merge(map, {}, target);
|
|
510
|
+
return target;
|
|
511
|
+
}
|
|
512
|
+
// 朴素对象
|
|
513
|
+
if (isPlainObject(target)) {
|
|
514
|
+
const exist = map.get(target);
|
|
515
|
+
if (exist)
|
|
516
|
+
return exist;
|
|
517
|
+
map.set(target, source);
|
|
518
|
+
objectEach(target, (val, key) => {
|
|
519
|
+
source[key] = merge(map, source[key], val);
|
|
520
|
+
});
|
|
521
|
+
return source;
|
|
522
|
+
}
|
|
523
|
+
// 数组
|
|
524
|
+
else if (isArray(target)) {
|
|
525
|
+
const exist = map.get(target);
|
|
526
|
+
if (exist)
|
|
527
|
+
return exist;
|
|
528
|
+
map.set(target, source);
|
|
529
|
+
target.forEach((val, index) => {
|
|
530
|
+
source[index] = merge(map, source[index], val);
|
|
531
|
+
});
|
|
532
|
+
return source;
|
|
533
|
+
}
|
|
534
|
+
return target;
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* 对象合并,返回原始对象
|
|
538
|
+
* @param {ObjectAssignItem} source
|
|
539
|
+
* @param {ObjectAssignItem | undefined} targets
|
|
540
|
+
* @returns {R}
|
|
541
|
+
*/
|
|
542
|
+
function objectAssign(source, ...targets) {
|
|
543
|
+
const map = new Map();
|
|
544
|
+
for (let i = 0, len = targets.length; i < len; i++) {
|
|
545
|
+
const target = targets[i];
|
|
546
|
+
// @ts-ignore
|
|
547
|
+
source = merge(map, source, target);
|
|
548
|
+
}
|
|
549
|
+
map.clear();
|
|
550
|
+
return source;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* 对象填充
|
|
554
|
+
* @param {Partial<R>} source
|
|
555
|
+
* @param {Partial<R>} target
|
|
556
|
+
* @param {(s: Partial<R>, t: Partial<R>, key: keyof R) => boolean} fillable
|
|
557
|
+
* @returns {R}
|
|
558
|
+
*/
|
|
559
|
+
function objectFill(source, target, fillable) {
|
|
560
|
+
const _fillable = fillable || ((source, target, key) => source[key] === undefined);
|
|
561
|
+
objectEach(target, (val, key) => {
|
|
562
|
+
if (_fillable(source, target, key)) {
|
|
563
|
+
source[key] = val;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
return source;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* 获取对象/数组指定层级下的属性值(现在可用ES6+的可选链?.来替代)
|
|
570
|
+
* @param {AnyObject} obj
|
|
571
|
+
* @param {string} path
|
|
572
|
+
* @param {boolean} strict
|
|
573
|
+
* @returns
|
|
574
|
+
*/
|
|
575
|
+
function objectGet(obj, path, strict = false) {
|
|
576
|
+
path = path.replace(/\[(\w+)\]/g, '.$1');
|
|
577
|
+
path = path.replace(/^\./, '');
|
|
578
|
+
const keyArr = path.split('.');
|
|
579
|
+
let tempObj = obj;
|
|
580
|
+
let i = 0;
|
|
581
|
+
for (let len = keyArr.length; i < len - 1; ++i) {
|
|
582
|
+
const key = keyArr[i];
|
|
583
|
+
if (isNumber(Number(key)) && Array.isArray(tempObj)) {
|
|
584
|
+
tempObj = tempObj[key];
|
|
585
|
+
}
|
|
586
|
+
else if (isObject(tempObj) && objectHas(tempObj, key)) {
|
|
587
|
+
tempObj = tempObj[key];
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
tempObj = undefined;
|
|
591
|
+
if (strict) {
|
|
592
|
+
throw new Error('[Object] objectGet path 路径不正确');
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
p: tempObj,
|
|
599
|
+
k: tempObj ? keyArr[i] : undefined,
|
|
600
|
+
v: tempObj ? tempObj[keyArr[i]] : undefined
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* 将字符串转换为驼峰格式
|
|
606
|
+
* @param {string} string
|
|
607
|
+
* @param {boolean} [bigger] 是否大写第一个字母
|
|
608
|
+
* @returns {string}
|
|
609
|
+
*/
|
|
610
|
+
function stringCamelCase(string, bigger) {
|
|
611
|
+
let string2 = string;
|
|
612
|
+
if (bigger) {
|
|
613
|
+
string2 = string.replace(/^./, origin => origin.toUpperCase());
|
|
614
|
+
}
|
|
615
|
+
const HUMP_RE = /[\s_-](.)/g;
|
|
616
|
+
return string2.replace(HUMP_RE, (orign, char) => char.toUpperCase());
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* 将字符串转换为连字格式
|
|
620
|
+
* @param {string} string
|
|
621
|
+
* @param {string} [separator] 分隔符,默认是"-"(短横线)
|
|
622
|
+
* @returns {string}
|
|
623
|
+
*/
|
|
624
|
+
function stringKebabCase(string, separator = '-') {
|
|
625
|
+
const string2 = string.replace(/^./, origin => origin.toLowerCase());
|
|
626
|
+
return string2.replace(/[A-Z]/g, origin => `${separator}${origin.toLowerCase()}`);
|
|
627
|
+
}
|
|
628
|
+
const STRING_ARABIC_NUMERALS = '0123456789';
|
|
629
|
+
const STRING_LOWERCASE_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
|
630
|
+
const STRING_UPPERCASE_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
631
|
+
const placeholderRE = /%[%sdo]/g;
|
|
632
|
+
/**
|
|
633
|
+
* 字符串格式化
|
|
634
|
+
* @example
|
|
635
|
+
* ```js
|
|
636
|
+
* stringFormat("My name is %s.", "zhangsan")
|
|
637
|
+
* // => "My name is zhangsan."
|
|
638
|
+
* ```
|
|
639
|
+
* @param {string} string 字符串模板,使用 %s 表示字符串,%d 表示数值,%o 表示对象,%% 表示百分号,参考 console.log
|
|
640
|
+
* @param args
|
|
641
|
+
* @returns {string}
|
|
642
|
+
*/
|
|
643
|
+
function stringFormat(string, ...args) {
|
|
644
|
+
let index = 0;
|
|
645
|
+
const result = string.replace(placeholderRE, (origin) => {
|
|
646
|
+
const arg = args[index++];
|
|
647
|
+
switch (origin) {
|
|
648
|
+
case '%%':
|
|
649
|
+
index--;
|
|
650
|
+
return '%';
|
|
651
|
+
default:
|
|
652
|
+
case '%s':
|
|
653
|
+
return String(arg);
|
|
654
|
+
case '%d':
|
|
655
|
+
return String(Number(arg));
|
|
656
|
+
case '%o':
|
|
657
|
+
return JSON.stringify(arg);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
return [result, ...args.splice(index).map(String)].join(' ');
|
|
661
|
+
}
|
|
662
|
+
const ev = (expression, data) => {
|
|
663
|
+
try {
|
|
664
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval,@typescript-eslint/no-unsafe-return
|
|
665
|
+
return new Function('with(arguments[0]){' +
|
|
666
|
+
/****/ `if(arguments[0].${expression} === undefined)throw "";` +
|
|
667
|
+
/****/
|
|
668
|
+
/****/ `return String(arguments[0].${expression})` +
|
|
669
|
+
'}')(data);
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
throw new SyntaxError(`无法执行表达式:${expression}`);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const templateRE = /\${(.*?)}/g;
|
|
676
|
+
/**
|
|
677
|
+
* 字符串赋值
|
|
678
|
+
* @example
|
|
679
|
+
* ```js
|
|
680
|
+
* stringAssign('My name is ${user}.', { user: 'zhangsan' });
|
|
681
|
+
* // => "My name is zhangsan."
|
|
682
|
+
* ```
|
|
683
|
+
* @param {string} template
|
|
684
|
+
* @param {AnyObject} data
|
|
685
|
+
* @returns {string}
|
|
686
|
+
*/
|
|
687
|
+
const stringAssign = (template, data) => {
|
|
688
|
+
return template.replace(templateRE, (origin, expression) => ev(expression, data));
|
|
689
|
+
};
|
|
690
|
+
/**
|
|
691
|
+
* 字符串编码 HTML
|
|
692
|
+
* @example
|
|
693
|
+
* ```js
|
|
694
|
+
* stringEscapeHtml('<b>You & Me speak "xixi"</b>')
|
|
695
|
+
* // => "<b>You & Me speak "xixi"</b>"
|
|
696
|
+
* ```
|
|
697
|
+
* @param {string} html
|
|
698
|
+
* @returns {string}
|
|
699
|
+
*/
|
|
700
|
+
const stringEscapeHtml = (html) => {
|
|
701
|
+
const htmlCharRE = /[&<>"]/g;
|
|
702
|
+
const htmlCharReplacements = {
|
|
703
|
+
'&': '&',
|
|
704
|
+
'<': '<',
|
|
705
|
+
'>': '>',
|
|
706
|
+
'"': '"'
|
|
707
|
+
};
|
|
708
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
709
|
+
// @ts-ignore
|
|
710
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
711
|
+
return html.replace(htmlCharRE, $0 => htmlCharReplacements[$0]);
|
|
712
|
+
};
|
|
713
|
+
/**
|
|
714
|
+
* 字符串填充
|
|
715
|
+
* @param {number} length
|
|
716
|
+
* @param {string} value
|
|
717
|
+
* @returns {string}
|
|
718
|
+
*/
|
|
719
|
+
const stringFill = (length, value = ' ') => new Array(length).fill(value).join('');
|
|
720
|
+
/**
|
|
721
|
+
* 解析URL查询参数
|
|
722
|
+
* @param {string} searchStr
|
|
723
|
+
* @returns {Record<string, string | string[]>}
|
|
724
|
+
*/
|
|
725
|
+
function parseQueryParams(searchStr = location.search) {
|
|
726
|
+
const queryObj = {};
|
|
727
|
+
Array.from(searchStr.matchAll(/[&?]?([^=&]+)=?([^=&]*)/g)).forEach((item, i) => {
|
|
728
|
+
if (!queryObj[item[1]]) {
|
|
729
|
+
queryObj[item[1]] = item[2];
|
|
730
|
+
}
|
|
731
|
+
else if (typeof queryObj[item[1]] === 'string') {
|
|
732
|
+
queryObj[item[1]] = [queryObj[item[1]], item[2]];
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
queryObj[item[1]].push(item[2]);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
return queryObj;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* 判断元素是否包含某个样式名
|
|
743
|
+
* @param {HTMLElement} el
|
|
744
|
+
* @param {string} className
|
|
745
|
+
* @returns {boolean}
|
|
746
|
+
*/
|
|
747
|
+
function hasClass(el, className) {
|
|
748
|
+
if (className.indexOf(' ') !== -1)
|
|
749
|
+
throw new Error('className should not contain space.');
|
|
750
|
+
return el.classList.contains(className);
|
|
751
|
+
}
|
|
752
|
+
const eachClassName = (classNames, func) => {
|
|
753
|
+
const classNameList = classNames.split(/\s+/g);
|
|
754
|
+
classNameList.forEach(func);
|
|
755
|
+
};
|
|
756
|
+
/**
|
|
757
|
+
* 给元素增加样式名
|
|
758
|
+
* @param {HTMLElement} el
|
|
759
|
+
* @param {string} classNames
|
|
760
|
+
*/
|
|
761
|
+
function addClass(el, classNames) {
|
|
762
|
+
eachClassName(classNames, className => el.classList.add(className));
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* 给元素移除样式名
|
|
766
|
+
* @param {HTMLElement} el
|
|
767
|
+
* @param {string} classNames
|
|
768
|
+
*/
|
|
769
|
+
function removeClass(el, classNames) {
|
|
770
|
+
eachClassName(classNames, className => el.classList.remove(className));
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* 设置元素样式
|
|
774
|
+
* @param {HTMLElement} el
|
|
775
|
+
* @param {string | Style} key
|
|
776
|
+
* @param {string} val
|
|
777
|
+
*/
|
|
778
|
+
const setStyle = (el, key, val) => {
|
|
779
|
+
if (isObject(key)) {
|
|
780
|
+
objectEach(key, (val1, key1) => {
|
|
781
|
+
setStyle(el, key1, val1);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
el.style.setProperty(stringKebabCase(key), val);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
/**
|
|
789
|
+
* 获取元素样式
|
|
790
|
+
* @param {HTMLElement} el 元素
|
|
791
|
+
* @param {string} key
|
|
792
|
+
* @returns {string}
|
|
793
|
+
*/
|
|
794
|
+
function getStyle(el, key) {
|
|
795
|
+
return getComputedStyle(el).getPropertyValue(key);
|
|
796
|
+
}
|
|
797
|
+
function smoothScroll(options) {
|
|
798
|
+
return new Promise(resolve => {
|
|
799
|
+
const defaults = {
|
|
800
|
+
el: document,
|
|
801
|
+
to: 0,
|
|
802
|
+
duration: 567,
|
|
803
|
+
easing: 'ease'
|
|
804
|
+
};
|
|
805
|
+
const { el, to, duration, easing } = objectAssign(defaults, options);
|
|
806
|
+
const htmlEl = document.documentElement;
|
|
807
|
+
const bodyEl = document.body;
|
|
808
|
+
const globalMode = el === window || el === document || el === htmlEl || el === bodyEl;
|
|
809
|
+
const els = globalMode ? [htmlEl, bodyEl] : [el];
|
|
810
|
+
const query = () => {
|
|
811
|
+
let value = 0;
|
|
812
|
+
arrayEach(els, el => {
|
|
813
|
+
if ('scrollTop' in el) {
|
|
814
|
+
value = el.scrollTop;
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
return value;
|
|
819
|
+
};
|
|
820
|
+
const update = (val) => {
|
|
821
|
+
els.forEach(el => {
|
|
822
|
+
if ('scrollTop' in el) {
|
|
823
|
+
el.scrollTop = val;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
let startTime;
|
|
828
|
+
const startValue = query();
|
|
829
|
+
const length = to - startValue;
|
|
830
|
+
const easingFn = easingFunctional(easing);
|
|
831
|
+
const render = () => {
|
|
832
|
+
const now = performance.now();
|
|
833
|
+
const passingTime = startTime ? now - startTime : 0;
|
|
834
|
+
const t = passingTime / duration;
|
|
835
|
+
const p = easingFn(t);
|
|
836
|
+
if (!startTime)
|
|
837
|
+
startTime = now;
|
|
838
|
+
update(startValue + length * p);
|
|
839
|
+
if (t >= 1)
|
|
840
|
+
resolve();
|
|
841
|
+
else
|
|
842
|
+
requestAnimationFrame(render);
|
|
843
|
+
};
|
|
844
|
+
render();
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* 获取元素样式属性的计算值
|
|
849
|
+
* @param {HTMLElement} el
|
|
850
|
+
* @param {string} property
|
|
851
|
+
* @param {boolean} reNumber
|
|
852
|
+
* @returns {string|number}
|
|
853
|
+
*/
|
|
854
|
+
function getComputedCssVal(el, property, reNumber = true) {
|
|
855
|
+
const originVal = getComputedStyle(el).getPropertyValue(property) ?? '';
|
|
856
|
+
return reNumber ? Number(originVal.replace(/([0-9]*)(.*)/g, '$1')) : originVal;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* 字符串的像素宽度
|
|
860
|
+
* @param {string} str 目标字符串
|
|
861
|
+
* @param {number} fontSize 字符串字体大小
|
|
862
|
+
* @param {boolean} isRemove 计算后是否移除创建的dom元素
|
|
863
|
+
* @returns {*}
|
|
864
|
+
*/
|
|
865
|
+
function getStrWidthPx(str, fontSize = 14, isRemove = true) {
|
|
866
|
+
let strWidth = 0;
|
|
867
|
+
console.assert(isString(str), `${str} 不是有效的字符串`);
|
|
868
|
+
if (isString(str) && str.length > 0) {
|
|
869
|
+
const id = 'getStrWidth1494304949567';
|
|
870
|
+
let getEle = document.querySelector(`#${id}`);
|
|
871
|
+
if (!getEle) {
|
|
872
|
+
const _ele = document.createElement('span');
|
|
873
|
+
_ele.id = id;
|
|
874
|
+
_ele.style.fontSize = fontSize + 'px';
|
|
875
|
+
_ele.style.whiteSpace = 'nowrap';
|
|
876
|
+
_ele.style.visibility = 'hidden';
|
|
877
|
+
_ele.style.position = 'absolute';
|
|
878
|
+
_ele.style.top = '-9999px';
|
|
879
|
+
_ele.style.left = '-9999px';
|
|
880
|
+
_ele.textContent = str;
|
|
881
|
+
document.body.appendChild(_ele);
|
|
882
|
+
getEle = _ele;
|
|
883
|
+
}
|
|
884
|
+
getEle.textContent = str;
|
|
885
|
+
strWidth = getEle.offsetWidth;
|
|
886
|
+
if (isRemove) {
|
|
887
|
+
getEle.remove();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return strWidth;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Programmatically select the text of a HTML element
|
|
894
|
+
*
|
|
895
|
+
* @param {HTMLElement} element The element whose text you wish to select
|
|
896
|
+
* @returns
|
|
897
|
+
*/
|
|
898
|
+
function select(element) {
|
|
899
|
+
let selectedText;
|
|
900
|
+
if (element.nodeName === 'SELECT') {
|
|
901
|
+
element.focus();
|
|
902
|
+
// @ts-ignore
|
|
903
|
+
selectedText = element.value;
|
|
904
|
+
}
|
|
905
|
+
else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
|
906
|
+
const isReadOnly = element.hasAttribute('readonly');
|
|
907
|
+
if (!isReadOnly) {
|
|
908
|
+
element.setAttribute('readonly', '');
|
|
909
|
+
}
|
|
910
|
+
// @ts-ignore
|
|
911
|
+
element.select();
|
|
912
|
+
// @ts-ignore
|
|
913
|
+
element.setSelectionRange(0, element.value.length);
|
|
914
|
+
if (!isReadOnly) {
|
|
915
|
+
element.removeAttribute('readonly');
|
|
916
|
+
}
|
|
917
|
+
// @ts-ignore
|
|
918
|
+
selectedText = element.value;
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
if (element.hasAttribute('contenteditable')) {
|
|
922
|
+
element.focus();
|
|
923
|
+
}
|
|
924
|
+
const selection = window.getSelection();
|
|
925
|
+
const range = document.createRange();
|
|
926
|
+
range.selectNodeContents(element);
|
|
927
|
+
selection.removeAllRanges();
|
|
928
|
+
selection.addRange(range);
|
|
929
|
+
selectedText = selection.toString();
|
|
930
|
+
}
|
|
931
|
+
return selectedText;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* 复制文本,优先使用navigator.clipboard,仅在安全上下文(HTTPS/localhost)下生效,若不支持则回退使用execCommand方式
|
|
936
|
+
* @param {string} text
|
|
937
|
+
* @param {CopyTextOptions} options 可选参数:成功回调successCallback、失败回调failCallback、容器元素container
|
|
938
|
+
* (默认document.body, 当不支持clipboard时必须传复制按钮元素,包裹模拟选择操作的临时元素,
|
|
939
|
+
* 解决脱离文档流的元素无法复制的问题,如Modal内复制操作)
|
|
940
|
+
*/
|
|
941
|
+
function copyText(text, options) {
|
|
942
|
+
const { successCallback = void 0, failCallback = void 0 } = isNullOrUnDef(options) ? {} : options;
|
|
943
|
+
if (navigator.clipboard) {
|
|
944
|
+
navigator.clipboard
|
|
945
|
+
.writeText(text)
|
|
946
|
+
.then(() => {
|
|
947
|
+
if (isFunction(successCallback)) {
|
|
948
|
+
successCallback();
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
.catch(err => {
|
|
952
|
+
fallbackCopyText(text, options);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// 使用旧版execCommand方法
|
|
957
|
+
fallbackCopyText(text, options);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* 使用execCommand方式复制文本
|
|
962
|
+
* @param text
|
|
963
|
+
* @param options
|
|
964
|
+
*/
|
|
965
|
+
function fallbackCopyText(text, options) {
|
|
966
|
+
const { successCallback = void 0, failCallback = void 0, container = document.body } = isNullOrUnDef(options) ? {} : options;
|
|
967
|
+
let textEl = createFakeElement(text);
|
|
968
|
+
container.appendChild(textEl);
|
|
969
|
+
select(textEl);
|
|
970
|
+
try {
|
|
971
|
+
const res = document.execCommand('copy');
|
|
972
|
+
if (res && isFunction(successCallback)) {
|
|
973
|
+
successCallback();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
if (isFunction(failCallback)) {
|
|
978
|
+
failCallback(err);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
finally {
|
|
982
|
+
container.removeChild(textEl);
|
|
983
|
+
// @ts-ignore
|
|
984
|
+
textEl = null;
|
|
985
|
+
window.getSelection()?.removeAllRanges(); // 清除选区
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Creates a fake textarea element with a value.
|
|
990
|
+
* @param {String} value
|
|
991
|
+
* @return {HTMLElement}
|
|
992
|
+
*/
|
|
993
|
+
function createFakeElement(value) {
|
|
994
|
+
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
|
995
|
+
const fakeElement = document.createElement('textarea');
|
|
996
|
+
// Prevent zooming on iOS
|
|
997
|
+
fakeElement.style.fontSize = '12pt';
|
|
998
|
+
// Reset box model
|
|
999
|
+
fakeElement.style.border = '0';
|
|
1000
|
+
fakeElement.style.padding = '0';
|
|
1001
|
+
fakeElement.style.margin = '0';
|
|
1002
|
+
// Move element out of screen horizontally
|
|
1003
|
+
fakeElement.style.position = 'absolute';
|
|
1004
|
+
fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px';
|
|
1005
|
+
// Move element to the same position vertically
|
|
1006
|
+
const yPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
1007
|
+
fakeElement.style.top = `${yPosition}px`;
|
|
1008
|
+
fakeElement.setAttribute('readonly', '');
|
|
1009
|
+
fakeElement.value = value;
|
|
1010
|
+
return fakeElement;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* 获取cookie
|
|
1015
|
+
* @param {string} name
|
|
1016
|
+
* @returns {string}
|
|
1017
|
+
*/
|
|
1018
|
+
function cookieGet(name) {
|
|
1019
|
+
const { cookie } = document;
|
|
1020
|
+
if (!cookie)
|
|
1021
|
+
return '';
|
|
1022
|
+
const result = cookie.split(';');
|
|
1023
|
+
for (let i = 0; i < result.length; i++) {
|
|
1024
|
+
const item = result[i];
|
|
1025
|
+
const [key, val = ''] = item.split('=');
|
|
1026
|
+
if (key === name)
|
|
1027
|
+
return decodeURIComponent(val);
|
|
1028
|
+
}
|
|
1029
|
+
return '';
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* 设置 cookie
|
|
1033
|
+
* @param {string} name
|
|
1034
|
+
* @param {string} value
|
|
1035
|
+
* @param {number | Date} [maxAge]
|
|
1036
|
+
*/
|
|
1037
|
+
function cookieSet(name, value, maxAge) {
|
|
1038
|
+
const metas = [];
|
|
1039
|
+
const EXPIRES = 'expires';
|
|
1040
|
+
metas.push([name, encodeURIComponent(value)]);
|
|
1041
|
+
if (isNumber(maxAge)) {
|
|
1042
|
+
const d = new Date();
|
|
1043
|
+
d.setTime(d.getTime() + maxAge);
|
|
1044
|
+
metas.push([EXPIRES, d.toUTCString()]);
|
|
1045
|
+
}
|
|
1046
|
+
else if (isDate(maxAge)) {
|
|
1047
|
+
metas.push([EXPIRES, maxAge.toUTCString()]);
|
|
1048
|
+
}
|
|
1049
|
+
metas.push(['path', '/']);
|
|
1050
|
+
document.cookie = metas
|
|
1051
|
+
.map(item => {
|
|
1052
|
+
const [key, val] = item;
|
|
1053
|
+
return `${key}=${val}`;
|
|
1054
|
+
})
|
|
1055
|
+
.join(';');
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* 删除单个 cookie
|
|
1059
|
+
* @param name cookie 名称
|
|
1060
|
+
*/
|
|
1061
|
+
const cookieDel = (name) => cookieSet(name, '', -1);
|
|
1062
|
+
|
|
1063
|
+
const isValidDate = (any) => isDate(any) && !isNaN(any.getTime());
|
|
1064
|
+
/* istanbul ignore next */
|
|
1065
|
+
const guessDateSeparator = (value) => {
|
|
1066
|
+
if (!isString(value))
|
|
1067
|
+
return;
|
|
1068
|
+
const value2 = value.replace(/-/g, '/');
|
|
1069
|
+
return new Date(value2);
|
|
1070
|
+
};
|
|
1071
|
+
/* istanbul ignore next */
|
|
1072
|
+
const guessDateTimezone = (value) => {
|
|
1073
|
+
if (!isString(value))
|
|
1074
|
+
return;
|
|
1075
|
+
const re = /([+-])(\d\d)(\d\d)$/;
|
|
1076
|
+
const matches = re.exec(value);
|
|
1077
|
+
if (!matches)
|
|
1078
|
+
return;
|
|
1079
|
+
const value2 = value.replace(re, 'Z');
|
|
1080
|
+
const d = new Date(value2);
|
|
1081
|
+
if (!isValidDate(d))
|
|
1082
|
+
return;
|
|
1083
|
+
const [, flag, hours, minutes] = matches;
|
|
1084
|
+
const hours2 = parseInt(hours, 10);
|
|
1085
|
+
const minutes2 = parseInt(minutes, 10);
|
|
1086
|
+
const offset = (a, b) => (flag === '+' ? a - b : a + b);
|
|
1087
|
+
d.setHours(offset(d.getHours(), hours2));
|
|
1088
|
+
d.setMinutes(offset(d.getMinutes(), minutes2));
|
|
1089
|
+
return d;
|
|
1090
|
+
};
|
|
1091
|
+
/**
|
|
1092
|
+
* 解析为Date对象
|
|
1093
|
+
* @param {DateValue} value - 可以是数值、字符串或 Date 对象
|
|
1094
|
+
* @returns {Date} - 转换后的目标Date
|
|
1095
|
+
*/
|
|
1096
|
+
function dateParse(value) {
|
|
1097
|
+
const d1 = new Date(value);
|
|
1098
|
+
if (isValidDate(d1))
|
|
1099
|
+
return d1;
|
|
1100
|
+
// safari 浏览器的日期解析有问题
|
|
1101
|
+
// new Date('2020-06-26 18:06:15') 返回值是一个非法日期对象
|
|
1102
|
+
/* istanbul ignore next */
|
|
1103
|
+
const d2 = guessDateSeparator(value);
|
|
1104
|
+
/* istanbul ignore next */
|
|
1105
|
+
if (isValidDate(d2))
|
|
1106
|
+
return d2;
|
|
1107
|
+
// safari 浏览器的日期解析有问题
|
|
1108
|
+
// new Date('2020-06-26T18:06:15.000+0800') 返回值是一个非法日期对象
|
|
1109
|
+
/* istanbul ignore next */
|
|
1110
|
+
const d3 = guessDateTimezone(value);
|
|
1111
|
+
/* istanbul ignore next */
|
|
1112
|
+
if (isValidDate(d3))
|
|
1113
|
+
return d3;
|
|
1114
|
+
throw new SyntaxError(`${value.toString()} 不是一个合法的日期描述`);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* 格式化为日期对象(带自定义格式化模板)
|
|
1118
|
+
* @param {DateValue} value 可以是数值、字符串或 Date 对象
|
|
1119
|
+
* @param {string} [format] 模板,默认是 YYYY-MM-DD HH:mm:ss,模板字符:
|
|
1120
|
+
* - YYYY:年
|
|
1121
|
+
* - yyyy: 年
|
|
1122
|
+
* - MM:月
|
|
1123
|
+
* - DD:日
|
|
1124
|
+
* - dd: 日
|
|
1125
|
+
* - HH:时(24 小时制)
|
|
1126
|
+
* - hh:时(12 小时制)
|
|
1127
|
+
* - mm:分
|
|
1128
|
+
* - ss:秒
|
|
1129
|
+
* - SSS:毫秒
|
|
1130
|
+
* @returns {string}
|
|
1131
|
+
*/
|
|
1132
|
+
// export const dateStringify = (value: DateValue, format = 'YYYY-MM-DD HH:mm:ss'): string => {
|
|
1133
|
+
// const date = dateParse(value);
|
|
1134
|
+
// let fmt = format;
|
|
1135
|
+
// let ret;
|
|
1136
|
+
// const opt: DateObj = {
|
|
1137
|
+
// 'Y+': `${date.getFullYear()}`, // 年
|
|
1138
|
+
// 'y+': `${date.getFullYear()}`, // 年
|
|
1139
|
+
// 'M+': `${date.getMonth() + 1}`, // 月
|
|
1140
|
+
// 'D+': `${date.getDate()}`, // 日
|
|
1141
|
+
// 'd+': `${date.getDate()}`, // 日
|
|
1142
|
+
// 'H+': `${date.getHours()}`, // 时
|
|
1143
|
+
// 'm+': `${date.getMinutes()}`, // 分
|
|
1144
|
+
// 's+': `${date.getSeconds()}`, // 秒
|
|
1145
|
+
// 'S+': `${date.getMilliseconds()}` // 豪秒
|
|
1146
|
+
// };
|
|
1147
|
+
// for (const k in opt) {
|
|
1148
|
+
// ret = new RegExp(`(${k})`).exec(fmt);
|
|
1149
|
+
// if (ret) {
|
|
1150
|
+
// fmt = fmt.replace(ret[1], ret[1].length === 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
|
|
1151
|
+
// }
|
|
1152
|
+
// }
|
|
1153
|
+
// return fmt;
|
|
1154
|
+
// };
|
|
1155
|
+
/**
|
|
1156
|
+
* 将日期转换为一天的开始时间,即0点0分0秒0毫秒
|
|
1157
|
+
* @param {DateValue} value
|
|
1158
|
+
* @returns {Date}
|
|
1159
|
+
*/
|
|
1160
|
+
function dateToStart(value) {
|
|
1161
|
+
const d = dateParse(value);
|
|
1162
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* 将日期转换为一天的结束时间,即23点59分59秒999毫秒
|
|
1166
|
+
* @param {DateValue} value
|
|
1167
|
+
* @returns {Date}
|
|
1168
|
+
*/
|
|
1169
|
+
function dateToEnd(value) {
|
|
1170
|
+
const d = dateToStart(value);
|
|
1171
|
+
d.setDate(d.getDate() + 1);
|
|
1172
|
+
return dateParse(d.getTime() - 1);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* 格式化为日期对象(带自定义格式化模板)
|
|
1176
|
+
* @param {Date} value - 可以是数值、字符串或 Date 对象
|
|
1177
|
+
* @param {string} [format] - 模板,默认是 YYYY-MM-DD HH:mm:ss,模板字符:
|
|
1178
|
+
* - YYYY:年
|
|
1179
|
+
* - yyyy: 年
|
|
1180
|
+
* - MM:月
|
|
1181
|
+
* - DD:日
|
|
1182
|
+
* - dd: 日
|
|
1183
|
+
* - HH:时(24 小时制)
|
|
1184
|
+
* - mm:分
|
|
1185
|
+
* - ss:秒
|
|
1186
|
+
* - SSS:毫秒
|
|
1187
|
+
* - ww: 周
|
|
1188
|
+
* @returns {string} 格式化后的日期字符串
|
|
1189
|
+
*/
|
|
1190
|
+
function formatDate(value, format = 'YYYY-MM-DD HH:mm:ss') {
|
|
1191
|
+
const date = dateParse(value);
|
|
1192
|
+
let fmt = format;
|
|
1193
|
+
let ret;
|
|
1194
|
+
const opt = {
|
|
1195
|
+
'Y+': `${date.getFullYear()}`,
|
|
1196
|
+
'y+': `${date.getFullYear()}`,
|
|
1197
|
+
'M+': `${date.getMonth() + 1}`,
|
|
1198
|
+
'D+': `${date.getDate()}`,
|
|
1199
|
+
'd+': `${date.getDate()}`,
|
|
1200
|
+
'H+': `${date.getHours()}`,
|
|
1201
|
+
'm+': `${date.getMinutes()}`,
|
|
1202
|
+
's+': `${date.getSeconds()}`,
|
|
1203
|
+
'S+': `${date.getMilliseconds()}`,
|
|
1204
|
+
'w+': ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()] // 周
|
|
1205
|
+
// 有其他格式化字符需求可以继续添加,必须转化成字符串
|
|
1206
|
+
};
|
|
1207
|
+
for (const k in opt) {
|
|
1208
|
+
ret = new RegExp('(' + k + ')').exec(fmt);
|
|
1209
|
+
if (ret) {
|
|
1210
|
+
fmt = fmt.replace(ret[1], ret[1].length === 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return fmt;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* 计算向前或向后N天的具体日期
|
|
1217
|
+
* @param {DateValue} originDate - 参考日期
|
|
1218
|
+
* @param {number} n - 正数:向后推算;负数:向前推算
|
|
1219
|
+
* @param {string} sep - 日期格式的分隔符
|
|
1220
|
+
* @returns {string} 计算后的目标日期
|
|
1221
|
+
*/
|
|
1222
|
+
function calculateDate(originDate, n, sep = '-') {
|
|
1223
|
+
//originDate 为字符串日期 如:'2019-01-01' n为你要传入的参数,当前为0,前一天为-1,后一天为1
|
|
1224
|
+
const date = new Date(originDate); //这边给定一个特定时间
|
|
1225
|
+
const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1226
|
+
const millisecondGap = newDate.getTime() + 1000 * 60 * 60 * 24 * parseInt(String(n)); //计算前几天用减,计算后几天用加,最后一个就是多少天的数量
|
|
1227
|
+
const targetDate = new Date(millisecondGap);
|
|
1228
|
+
const finalNewDate = targetDate.getFullYear() +
|
|
1229
|
+
sep +
|
|
1230
|
+
String(targetDate.getMonth() + 1).padStart(2, '0') +
|
|
1231
|
+
'-' +
|
|
1232
|
+
String(targetDate.getDate()).padStart(2, '0');
|
|
1233
|
+
return finalNewDate;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* 计算向前或向后N天的具体日期时间
|
|
1237
|
+
* @param {DateValue} originDateTime - 参考日期时间
|
|
1238
|
+
* @param {number} n - 正数:向后推算;负数:向前推算
|
|
1239
|
+
* @param {string} dateSep - 日期分隔符
|
|
1240
|
+
* @param {string} timeSep - 时间分隔符
|
|
1241
|
+
* @returns {string} 转换后的目标日期时间
|
|
1242
|
+
*/
|
|
1243
|
+
function calculateDateTime(originDateTime, n, dateSep = '-', timeSep = ':') {
|
|
1244
|
+
const date = new Date(originDateTime);
|
|
1245
|
+
const separator1 = dateSep;
|
|
1246
|
+
const separator2 = timeSep;
|
|
1247
|
+
const dateTime = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
|
1248
|
+
const millisecondGap = dateTime.getTime() + 1000 * 60 * 60 * 24 * parseInt(String(n)); //计算前几天用减,计算后几天用加,最后一个就是多少天的数量
|
|
1249
|
+
const targetDateTime = new Date(millisecondGap);
|
|
1250
|
+
return (targetDateTime.getFullYear() +
|
|
1251
|
+
separator1 +
|
|
1252
|
+
String(targetDateTime.getMonth() + 1).padStart(2, '0') +
|
|
1253
|
+
separator1 +
|
|
1254
|
+
String(targetDateTime.getDate()).padStart(2, '0') +
|
|
1255
|
+
' ' +
|
|
1256
|
+
String(targetDateTime.getHours()).padStart(2, '0') +
|
|
1257
|
+
separator2 +
|
|
1258
|
+
String(targetDateTime.getMinutes()).padStart(2, '0') +
|
|
1259
|
+
separator2 +
|
|
1260
|
+
String(targetDateTime.getSeconds()).padStart(2, '0'));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* 标准化路径
|
|
1265
|
+
* @param {string} path
|
|
1266
|
+
* @returns {string}
|
|
1267
|
+
*/
|
|
1268
|
+
const pathNormalize = (path) => {
|
|
1269
|
+
const slices = path
|
|
1270
|
+
.replace(/\\/g, '/')
|
|
1271
|
+
.replace(/\/{2,}/g, '/')
|
|
1272
|
+
.replace(/\.{3,}/g, '..')
|
|
1273
|
+
.replace(/\/\.\//g, '/')
|
|
1274
|
+
.split('/')
|
|
1275
|
+
.map(point => point.trim());
|
|
1276
|
+
const isCurrentSlice = (slice) => slice === '.';
|
|
1277
|
+
const isParentSlice = (slice) => slice === '..';
|
|
1278
|
+
const points = [];
|
|
1279
|
+
let inPoints = false;
|
|
1280
|
+
const push = (point) => {
|
|
1281
|
+
points.push(point);
|
|
1282
|
+
};
|
|
1283
|
+
const back = () => {
|
|
1284
|
+
if (points.length === 0)
|
|
1285
|
+
return;
|
|
1286
|
+
const lastSlice = points[points.length - 1];
|
|
1287
|
+
if (isParentSlice(lastSlice)) {
|
|
1288
|
+
points.push('..');
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
points.pop();
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
slices.forEach(slice => {
|
|
1295
|
+
const isCurrent = isCurrentSlice(slice);
|
|
1296
|
+
const isParent = isParentSlice(slice);
|
|
1297
|
+
// 未进入实际路径
|
|
1298
|
+
if (!inPoints) {
|
|
1299
|
+
push(slice);
|
|
1300
|
+
inPoints = !isCurrent && !isParent;
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (isCurrent)
|
|
1304
|
+
return;
|
|
1305
|
+
if (isParent)
|
|
1306
|
+
return back();
|
|
1307
|
+
push(slice);
|
|
1308
|
+
});
|
|
1309
|
+
return points.join('/');
|
|
1310
|
+
};
|
|
1311
|
+
/**
|
|
1312
|
+
* 路径合并
|
|
1313
|
+
* @param {string} from
|
|
1314
|
+
* @param {string} to
|
|
1315
|
+
* @returns {string}
|
|
1316
|
+
*/
|
|
1317
|
+
const pathJoin = (from, ...to) => pathNormalize([from, ...to].join('/'));
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* 解析查询参数,内部使用的是浏览器内置的 URLSearchParams 进行处理
|
|
1321
|
+
* @param {string} queryString
|
|
1322
|
+
* @returns {Params}
|
|
1323
|
+
*/
|
|
1324
|
+
function qsParse(queryString) {
|
|
1325
|
+
const params = new URLSearchParams(queryString);
|
|
1326
|
+
const result = {};
|
|
1327
|
+
for (const [key, val] of params.entries()) {
|
|
1328
|
+
if (isUndefined(result[key])) {
|
|
1329
|
+
result[key] = val;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (isArray(result[key])) {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
result[key] = params.getAll(key);
|
|
1336
|
+
}
|
|
1337
|
+
return result;
|
|
1338
|
+
}
|
|
1339
|
+
const defaultReplacer = (val) => {
|
|
1340
|
+
if (isString(val))
|
|
1341
|
+
return val;
|
|
1342
|
+
if (isNumber(val))
|
|
1343
|
+
return String(val);
|
|
1344
|
+
if (isBoolean(val))
|
|
1345
|
+
return val ? 'true' : 'false';
|
|
1346
|
+
if (isDate(val))
|
|
1347
|
+
return val.toISOString();
|
|
1348
|
+
return null;
|
|
1349
|
+
};
|
|
1350
|
+
/**
|
|
1351
|
+
* 字符化查询对象,内部使用的是浏览器内置的 URLSearchParams 进行处理
|
|
1352
|
+
* @param {LooseParams} query
|
|
1353
|
+
* @param {Replacer} replacer
|
|
1354
|
+
* @returns {string}
|
|
1355
|
+
*/
|
|
1356
|
+
function qsStringify(query, replacer = defaultReplacer) {
|
|
1357
|
+
const params = new URLSearchParams();
|
|
1358
|
+
objectEach(query, (val, key) => {
|
|
1359
|
+
if (isArray(val)) {
|
|
1360
|
+
val.forEach(i => {
|
|
1361
|
+
const replaced = replacer(i);
|
|
1362
|
+
if (replaced === null)
|
|
1363
|
+
return;
|
|
1364
|
+
params.append(key.toString(), replaced);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
const replaced = replacer(val);
|
|
1369
|
+
if (replaced === null)
|
|
1370
|
+
return;
|
|
1371
|
+
params.set(key.toString(), replaced);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
return params.toString();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* url 解析
|
|
1379
|
+
* @param {string} url
|
|
1380
|
+
* @param {boolean} isModernApi 使用现代API:URL, 默认true (对无效url解析会抛错), 否则使用a标签来解析(兼容性更强)
|
|
1381
|
+
* @returns {Url}
|
|
1382
|
+
*/
|
|
1383
|
+
const urlParse = (url, isModernApi = true) => {
|
|
1384
|
+
// @ts-ignore
|
|
1385
|
+
let urlObj = null;
|
|
1386
|
+
if (isFunction(URL) && isModernApi) {
|
|
1387
|
+
urlObj = new URL(url);
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
urlObj = document.createElement('a');
|
|
1391
|
+
urlObj.href = url;
|
|
1392
|
+
}
|
|
1393
|
+
const { protocol, username, password, host, port, hostname, hash, search, pathname: _pathname } = urlObj;
|
|
1394
|
+
// fix: ie 浏览器下,解析出来的 pathname 是没有 / 根的
|
|
1395
|
+
const pathname = pathJoin('/', _pathname);
|
|
1396
|
+
const auth = username && password ? `${username}:${password}` : '';
|
|
1397
|
+
const query = search.replace(/^\?/, '');
|
|
1398
|
+
const searchParams = qsParse(query);
|
|
1399
|
+
const path = `${pathname}${search}`;
|
|
1400
|
+
urlObj = null;
|
|
1401
|
+
return {
|
|
1402
|
+
protocol,
|
|
1403
|
+
auth,
|
|
1404
|
+
username,
|
|
1405
|
+
password,
|
|
1406
|
+
host,
|
|
1407
|
+
port,
|
|
1408
|
+
hostname,
|
|
1409
|
+
hash,
|
|
1410
|
+
search,
|
|
1411
|
+
searchParams,
|
|
1412
|
+
query,
|
|
1413
|
+
pathname,
|
|
1414
|
+
path,
|
|
1415
|
+
href: url
|
|
1416
|
+
};
|
|
1417
|
+
};
|
|
1418
|
+
/**
|
|
1419
|
+
* url 字符化,url 对象里的 searchParams 会覆盖 url 原有的查询参数
|
|
1420
|
+
* @param {Url} url
|
|
1421
|
+
* @returns {string}
|
|
1422
|
+
*/
|
|
1423
|
+
const urlStringify = (url) => {
|
|
1424
|
+
const { protocol, auth, host, pathname, searchParams, hash } = url;
|
|
1425
|
+
const authorize = auth ? `${auth}@` : '';
|
|
1426
|
+
const querystring = qsStringify(searchParams);
|
|
1427
|
+
const search = querystring ? `?${querystring}` : '';
|
|
1428
|
+
let hashstring = hash.replace(/^#/, '');
|
|
1429
|
+
hashstring = hashstring ? '#' + hashstring : '';
|
|
1430
|
+
return `${protocol}//${authorize}${host}${pathname}${search}${hashstring}`;
|
|
1431
|
+
};
|
|
1432
|
+
/**
|
|
1433
|
+
* 设置 url 查询参数
|
|
1434
|
+
* @param {string} url
|
|
1435
|
+
* @param {AnyObject} setter
|
|
1436
|
+
* @returns {string}
|
|
1437
|
+
*/
|
|
1438
|
+
const urlSetParams = (url, setter) => {
|
|
1439
|
+
const p = urlParse(url);
|
|
1440
|
+
Object.assign(p.searchParams, setter);
|
|
1441
|
+
return urlStringify(p);
|
|
1442
|
+
};
|
|
1443
|
+
/**
|
|
1444
|
+
* 删除 url 查询参数
|
|
1445
|
+
* @param {string} url
|
|
1446
|
+
* @param {string[]} removeKeys
|
|
1447
|
+
* @returns {string}
|
|
1448
|
+
*/
|
|
1449
|
+
const urlDelParams = (url, removeKeys) => {
|
|
1450
|
+
const p = urlParse(url);
|
|
1451
|
+
removeKeys.forEach(key => delete p.searchParams[key]);
|
|
1452
|
+
return urlStringify(p);
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* 通过打开新窗口的方式下载
|
|
1457
|
+
* @param {string} url
|
|
1458
|
+
* @param {LooseParams} params
|
|
1459
|
+
*/
|
|
1460
|
+
function downloadURL(url, params) {
|
|
1461
|
+
window.open(params ? urlSetParams(url, params) : url);
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* 通过 A 链接的方式下载
|
|
1465
|
+
* @param {string} href
|
|
1466
|
+
* @param {string} filename
|
|
1467
|
+
* @param {Function} callback
|
|
1468
|
+
*/
|
|
1469
|
+
function downloadHref(href, filename, callback) {
|
|
1470
|
+
let eleLink = document.createElement('a');
|
|
1471
|
+
eleLink.download = filename;
|
|
1472
|
+
eleLink.style.display = 'none';
|
|
1473
|
+
eleLink.href = href;
|
|
1474
|
+
document.body.appendChild(eleLink);
|
|
1475
|
+
eleLink.click();
|
|
1476
|
+
setTimeout(() => {
|
|
1477
|
+
document.body.removeChild(eleLink);
|
|
1478
|
+
// @ts-ignore
|
|
1479
|
+
eleLink = null;
|
|
1480
|
+
if (isFunction(callback)) {
|
|
1481
|
+
callback();
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* 将大文件对象通过 A 链接的方式下载
|
|
1487
|
+
* @param {Blob} blob
|
|
1488
|
+
* @param {string} filename
|
|
1489
|
+
* @param {Function} callback
|
|
1490
|
+
*/
|
|
1491
|
+
function downloadBlob(blob, filename, callback) {
|
|
1492
|
+
const objURL = URL.createObjectURL(blob);
|
|
1493
|
+
downloadHref(objURL, filename);
|
|
1494
|
+
setTimeout(() => {
|
|
1495
|
+
URL.revokeObjectURL(objURL);
|
|
1496
|
+
if (isFunction(callback)) {
|
|
1497
|
+
callback();
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* 根据URL下载文件(解决跨域a.download不生效问题)
|
|
1503
|
+
*
|
|
1504
|
+
* 可定制下载成功的状态码status(浏览器原生状态码)
|
|
1505
|
+
*
|
|
1506
|
+
* 支持下载操作成功、失败后的回调
|
|
1507
|
+
* @param {string} url
|
|
1508
|
+
* @param {string} filename
|
|
1509
|
+
* @param {CrossOriginDownloadParams} options
|
|
1510
|
+
*/
|
|
1511
|
+
function crossOriginDownload(url, filename, options) {
|
|
1512
|
+
const { successCode = 200, successCallback, failCallback } = isNullOrUnDef(options) ? { successCode: 200, successCallback: void 0, failCallback: void 0 } : options;
|
|
1513
|
+
const xhr = new XMLHttpRequest();
|
|
1514
|
+
xhr.open('GET', url, true);
|
|
1515
|
+
xhr.responseType = 'blob';
|
|
1516
|
+
xhr.onload = function () {
|
|
1517
|
+
if (xhr.status === successCode)
|
|
1518
|
+
downloadBlob(xhr.response, filename, successCallback);
|
|
1519
|
+
else if (isFunction(failCallback)) {
|
|
1520
|
+
const status = xhr.status;
|
|
1521
|
+
const responseType = xhr.getResponseHeader('Content-Type');
|
|
1522
|
+
if (isString(responseType) && responseType.includes('application/json')) {
|
|
1523
|
+
const reader = new FileReader();
|
|
1524
|
+
reader.onload = () => {
|
|
1525
|
+
failCallback({ status, response: reader.result });
|
|
1526
|
+
};
|
|
1527
|
+
reader.readAsText(xhr.response);
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
failCallback(xhr);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
xhr.onerror = e => {
|
|
1535
|
+
if (isFunction(failCallback)) {
|
|
1536
|
+
failCallback({ status: 0, code: 'ERROR_CONNECTION_REFUSED' });
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
xhr.send();
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* 将指定数据格式通过 A 链接的方式下载
|
|
1543
|
+
* @param {AnyObject | AnyObject[]} data
|
|
1544
|
+
* @param {FileType} fileType 支持 json/csv/xls/xlsx 四种格式
|
|
1545
|
+
* @param {string} filename
|
|
1546
|
+
* @param {string[]} [headers]
|
|
1547
|
+
*/
|
|
1548
|
+
function downloadData(data, fileType, filename, headers) {
|
|
1549
|
+
filename = filename.replace(`.${fileType}`, '') + `.${fileType}`;
|
|
1550
|
+
if (fileType === 'json') {
|
|
1551
|
+
const blob = new Blob([JSON.stringify(data, null, 4)]);
|
|
1552
|
+
downloadBlob(blob, filename);
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
// xlsx实际生成的也为csv,仅后缀名名不同
|
|
1556
|
+
if (!headers || !headers.length)
|
|
1557
|
+
throw new Error('未传入表头数据');
|
|
1558
|
+
if (!Array.isArray(data))
|
|
1559
|
+
throw new Error('data error! expected array!');
|
|
1560
|
+
const headerStr = headers.join(',') + '\n';
|
|
1561
|
+
let bodyStr = '';
|
|
1562
|
+
data.forEach(row => {
|
|
1563
|
+
// \t防止数字被科学计数法显示
|
|
1564
|
+
bodyStr += Object.values(row).join(',\t') + ',\n';
|
|
1565
|
+
});
|
|
1566
|
+
const MIMETypes = {
|
|
1567
|
+
csv: 'text/csv',
|
|
1568
|
+
xls: 'application/vnd.ms-excel',
|
|
1569
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
1570
|
+
};
|
|
1571
|
+
// encodeURIComponent解决中文乱码
|
|
1572
|
+
const href = 'data:' + MIMETypes[fileType] + ';charset=utf-8,\ufeff' + encodeURIComponent(headerStr + bodyStr);
|
|
1573
|
+
downloadHref(href, filename);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* 等待一段时间
|
|
1579
|
+
* @param {number} timeout 等待时间,单位毫秒
|
|
1580
|
+
* @returns {Promise<void>}
|
|
1581
|
+
*/
|
|
1582
|
+
function wait(timeout = 1) {
|
|
1583
|
+
return new Promise(resolve => setTimeout(resolve, timeout));
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* 异步遍历
|
|
1587
|
+
* @ref https://github.com/Kevnz/async-tools/blob/master/src/mapper.js
|
|
1588
|
+
* @ref https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator
|
|
1589
|
+
* @param {Array<T>} list
|
|
1590
|
+
* @param {(val: T, idx: number, list: ArrayLike<T>) => Promise<R>} mapper
|
|
1591
|
+
* @param {number} concurrency 并发数量,默认无限
|
|
1592
|
+
* @returns {Promise<R[]>}
|
|
1593
|
+
*/
|
|
1594
|
+
function asyncMap(list, mapper, concurrency = Infinity) {
|
|
1595
|
+
return new Promise((resolve, reject) => {
|
|
1596
|
+
const iterator = list[Symbol.iterator]();
|
|
1597
|
+
const limit = Math.min(list.length, concurrency);
|
|
1598
|
+
const resolves = [];
|
|
1599
|
+
let resolvedLength = 0;
|
|
1600
|
+
let rejected;
|
|
1601
|
+
let index = 0;
|
|
1602
|
+
const next = () => {
|
|
1603
|
+
if (rejected)
|
|
1604
|
+
return reject(rejected);
|
|
1605
|
+
const it = iterator.next();
|
|
1606
|
+
if (it.done) {
|
|
1607
|
+
if (resolvedLength === list.length)
|
|
1608
|
+
resolve(resolves);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const current = index++;
|
|
1612
|
+
mapper(it.value, current, list)
|
|
1613
|
+
.then(value => {
|
|
1614
|
+
resolvedLength++;
|
|
1615
|
+
resolves[current] = value;
|
|
1616
|
+
next();
|
|
1617
|
+
})
|
|
1618
|
+
.catch(err => {
|
|
1619
|
+
rejected = err;
|
|
1620
|
+
next();
|
|
1621
|
+
});
|
|
1622
|
+
};
|
|
1623
|
+
// 开始
|
|
1624
|
+
for (let i = 0; i < limit; i++) {
|
|
1625
|
+
next();
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Execute a promise safely
|
|
1631
|
+
*
|
|
1632
|
+
* @param { Promise } promise
|
|
1633
|
+
* @param { Object= } errorExt - Additional Information you can pass safeAwait the err object
|
|
1634
|
+
* @return { Promise }
|
|
1635
|
+
* @example
|
|
1636
|
+
* async function asyncTaskWithCb(cb) {
|
|
1637
|
+
let err, user, savedTask, notification;
|
|
1638
|
+
|
|
1639
|
+
[ err, user ] = await safeAwait(UserModel.findById(1));
|
|
1640
|
+
if(!user) return cb('No user found');
|
|
1641
|
+
|
|
1642
|
+
[ err, savedTask ] = await safeAwait(TaskModel({userId: user.id, name: 'Demo Task'}));
|
|
1643
|
+
if(err) return cb('Error occurred while saving task')
|
|
1644
|
+
|
|
1645
|
+
cb(null, savedTask);
|
|
1646
|
+
}
|
|
1647
|
+
*/
|
|
1648
|
+
function safeAwait(promise, errorExt) {
|
|
1649
|
+
return promise
|
|
1650
|
+
.then((data) => [null, data])
|
|
1651
|
+
.catch((err) => {
|
|
1652
|
+
if (errorExt) {
|
|
1653
|
+
const parsedError = Object.assign({}, err, errorExt);
|
|
1654
|
+
return [parsedError, undefined];
|
|
1655
|
+
}
|
|
1656
|
+
return [err, undefined];
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* 判断是否支持canvas
|
|
1662
|
+
* @returns {boolean}
|
|
1663
|
+
*/
|
|
1664
|
+
function supportCanvas() {
|
|
1665
|
+
return !!document.createElement('canvas').getContext;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* 选择本地文件
|
|
1669
|
+
* @param {string} accept 上传的文件类型,用于过滤
|
|
1670
|
+
* @param {Function} changeCb 选择文件回调
|
|
1671
|
+
* @returns {HTMLInputElement}
|
|
1672
|
+
*/
|
|
1673
|
+
function chooseLocalFile(accept, changeCb) {
|
|
1674
|
+
let inputObj = document.createElement('input');
|
|
1675
|
+
inputObj.setAttribute('id', String(Date.now()));
|
|
1676
|
+
inputObj.setAttribute('type', 'file');
|
|
1677
|
+
inputObj.setAttribute('style', 'visibility:hidden');
|
|
1678
|
+
inputObj.setAttribute('accept', accept);
|
|
1679
|
+
document.body.appendChild(inputObj);
|
|
1680
|
+
inputObj.click();
|
|
1681
|
+
// @ts-ignore
|
|
1682
|
+
inputObj.onchange = (e) => {
|
|
1683
|
+
changeCb(e.target.files);
|
|
1684
|
+
setTimeout(() => {
|
|
1685
|
+
document.body.removeChild(inputObj);
|
|
1686
|
+
// @ts-ignore
|
|
1687
|
+
inputObj = null;
|
|
1688
|
+
});
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* 计算图片压缩后的尺寸
|
|
1693
|
+
*
|
|
1694
|
+
* @param {number} maxWidth
|
|
1695
|
+
* @param {number} maxHeight
|
|
1696
|
+
* @param {number} originWidth
|
|
1697
|
+
* @param {number} originHeight
|
|
1698
|
+
* @returns {*}
|
|
1699
|
+
*/
|
|
1700
|
+
function calculateSize({ maxWidth, maxHeight, originWidth, originHeight }) {
|
|
1701
|
+
let width = originWidth, height = originHeight;
|
|
1702
|
+
// 图片尺寸超过限制
|
|
1703
|
+
if (originWidth > maxWidth || originHeight > maxHeight) {
|
|
1704
|
+
if (originWidth / originHeight > maxWidth / maxHeight) {
|
|
1705
|
+
// 更宽,按照宽度限定尺寸
|
|
1706
|
+
width = maxWidth;
|
|
1707
|
+
height = Math.round(maxWidth * (originHeight / originWidth));
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
height = maxHeight;
|
|
1711
|
+
width = Math.round(maxHeight * (originWidth / originHeight));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return { width, height };
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* 根据原始图片的不同尺寸计算等比例缩放后的宽高尺寸
|
|
1718
|
+
*
|
|
1719
|
+
* @param {number} sizeKB Image volume size, unit KB
|
|
1720
|
+
* @param {number} maxSize Image max size
|
|
1721
|
+
* @param {number} originWidth Image original width, unit px
|
|
1722
|
+
* @param {number} originHeight Image original height, unit px
|
|
1723
|
+
* @returns {*} {width, height}
|
|
1724
|
+
*/
|
|
1725
|
+
function scalingByAspectRatio({ sizeKB, maxSize, originWidth, originHeight }) {
|
|
1726
|
+
let targetWidth = originWidth, targetHeight = originHeight;
|
|
1727
|
+
if (isNumber(maxSize)) {
|
|
1728
|
+
const { width, height } = calculateSize({ maxWidth: maxSize, maxHeight: maxSize, originWidth, originHeight });
|
|
1729
|
+
targetWidth = width;
|
|
1730
|
+
targetHeight = height;
|
|
1731
|
+
}
|
|
1732
|
+
else if (sizeKB < 500) {
|
|
1733
|
+
// [50KB, 500KB)
|
|
1734
|
+
const maxWidth = 1200, maxHeight = 1200;
|
|
1735
|
+
const { width, height } = calculateSize({ maxWidth, maxHeight, originWidth, originHeight });
|
|
1736
|
+
targetWidth = width;
|
|
1737
|
+
targetHeight = height;
|
|
1738
|
+
}
|
|
1739
|
+
else if (sizeKB < 5 * 1024) {
|
|
1740
|
+
// [500KB, 5MB)
|
|
1741
|
+
const maxWidth = 1400, maxHeight = 1400;
|
|
1742
|
+
const { width, height } = calculateSize({ maxWidth, maxHeight, originWidth, originHeight });
|
|
1743
|
+
targetWidth = width;
|
|
1744
|
+
targetHeight = height;
|
|
1745
|
+
}
|
|
1746
|
+
else if (sizeKB < 10 * 1024) {
|
|
1747
|
+
// [5MB, 10MB)
|
|
1748
|
+
const maxWidth = 1600, maxHeight = 1600;
|
|
1749
|
+
const { width, height } = calculateSize({ maxWidth, maxHeight, originWidth, originHeight });
|
|
1750
|
+
targetWidth = width;
|
|
1751
|
+
targetHeight = height;
|
|
1752
|
+
}
|
|
1753
|
+
else if (10 * 1024 <= sizeKB) {
|
|
1754
|
+
// [10MB, Infinity)
|
|
1755
|
+
const maxWidth = originWidth > 15000 ? 8192 : originWidth > 10000 ? 4096 : 2048, maxHeight = originHeight > 15000 ? 8192 : originHeight > 10000 ? 4096 : 2048;
|
|
1756
|
+
const { width, height } = calculateSize({ maxWidth, maxHeight, originWidth, originHeight });
|
|
1757
|
+
targetWidth = width;
|
|
1758
|
+
targetHeight = height;
|
|
1759
|
+
}
|
|
1760
|
+
return { width: targetWidth, height: targetHeight };
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Web端:等比例压缩图片批量处理 (小于minFileSizeKB:50,不压缩), 支持压缩全景图或长截图
|
|
1764
|
+
*
|
|
1765
|
+
* 1. 默认根据图片原始size及宽高适当地调整quality、width、height
|
|
1766
|
+
* 2. 可指定压缩的图片质量 quality(若不指定则根据原始图片大小来计算), 来适当调整width、height
|
|
1767
|
+
* 3. 可指定压缩的图片最大宽高 maxSize(若不指定则根据原始图片宽高来计算), 满足大屏幕图片展示的场景
|
|
1768
|
+
*
|
|
1769
|
+
* @param {File | FileList} file 图片或图片数组
|
|
1770
|
+
* @param {ICompressOptions} options 压缩图片配置项,default: {mime:'image/jpeg', minFileSizeKB: 50}
|
|
1771
|
+
* @returns {Promise<ICompressImgResult | ICompressImgResult[] | null>}
|
|
1772
|
+
*/
|
|
1773
|
+
function compressImg(file, options = { mime: 'image/jpeg', minFileSizeKB: 50 }) {
|
|
1774
|
+
if (!(file instanceof File || file instanceof FileList)) {
|
|
1775
|
+
throw new Error(`${file} require be File or FileList`);
|
|
1776
|
+
}
|
|
1777
|
+
else if (!supportCanvas()) {
|
|
1778
|
+
throw new Error(`Current runtime environment not support Canvas`);
|
|
1779
|
+
}
|
|
1780
|
+
const { quality, mime = 'image/jpeg', maxSize: size, minFileSizeKB = 50 } = isObject(options) ? options : {};
|
|
1781
|
+
let targetQuality = quality, maxSize;
|
|
1782
|
+
if (quality) {
|
|
1783
|
+
targetQuality = quality;
|
|
1784
|
+
}
|
|
1785
|
+
else if (file instanceof File) {
|
|
1786
|
+
const sizeKB = +parseInt((file.size / 1024).toFixed(2));
|
|
1787
|
+
if (sizeKB < minFileSizeKB) {
|
|
1788
|
+
targetQuality = 1;
|
|
1789
|
+
}
|
|
1790
|
+
else if (sizeKB < 1 * 1024) {
|
|
1791
|
+
targetQuality = 0.85;
|
|
1792
|
+
}
|
|
1793
|
+
else if (sizeKB < 5 * 1024) {
|
|
1794
|
+
targetQuality = 0.8;
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
targetQuality = 0.75;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (isNumber(size)) {
|
|
1801
|
+
maxSize = size >= 1200 ? size : 1200;
|
|
1802
|
+
}
|
|
1803
|
+
if (file instanceof FileList) {
|
|
1804
|
+
return Promise.all(Array.from(file).map(el => compressImg(el, { maxSize, mime: mime, quality: targetQuality }))); // 如果是 file 数组返回 Promise 数组
|
|
1805
|
+
}
|
|
1806
|
+
else if (file instanceof File) {
|
|
1807
|
+
return new Promise(resolve => {
|
|
1808
|
+
const ext = {
|
|
1809
|
+
'image/webp': 'webp',
|
|
1810
|
+
'image/jpeg': 'jpg',
|
|
1811
|
+
'image/png': 'png'
|
|
1812
|
+
};
|
|
1813
|
+
const fileName = [...file.name.split('.').slice(0, -1), ext[mime]].join('.');
|
|
1814
|
+
const sizeKB = +parseInt((file.size / 1024).toFixed(2));
|
|
1815
|
+
if (sizeKB < minFileSizeKB) {
|
|
1816
|
+
resolve({
|
|
1817
|
+
file: file
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
const reader = new FileReader(); // 创建 FileReader
|
|
1822
|
+
// @ts-ignore
|
|
1823
|
+
reader.onload = ({ target: { result: src } }) => {
|
|
1824
|
+
const image = new Image(); // 创建 img 元素
|
|
1825
|
+
image.onload = () => {
|
|
1826
|
+
const canvas = document.createElement('canvas'); // 创建 canvas 元素
|
|
1827
|
+
const context = canvas.getContext('2d');
|
|
1828
|
+
const originWidth = image.width;
|
|
1829
|
+
const originHeight = image.height;
|
|
1830
|
+
const { width, height } = scalingByAspectRatio({ sizeKB, maxSize, originWidth, originHeight });
|
|
1831
|
+
canvas.width = width;
|
|
1832
|
+
canvas.height = height;
|
|
1833
|
+
context.clearRect(0, 0, width, height);
|
|
1834
|
+
context.drawImage(image, 0, 0, width, height); // 绘制 canvas
|
|
1835
|
+
const canvasURL = canvas.toDataURL(mime, targetQuality);
|
|
1836
|
+
const buffer = atob(canvasURL.split(',')[1]);
|
|
1837
|
+
let length = buffer.length;
|
|
1838
|
+
const bufferArray = new Uint8Array(new ArrayBuffer(length));
|
|
1839
|
+
while (length--) {
|
|
1840
|
+
bufferArray[length] = buffer.charCodeAt(length);
|
|
1841
|
+
}
|
|
1842
|
+
const miniFile = new File([bufferArray], fileName, {
|
|
1843
|
+
type: mime
|
|
1844
|
+
});
|
|
1845
|
+
resolve({
|
|
1846
|
+
file: miniFile,
|
|
1847
|
+
bufferArray,
|
|
1848
|
+
origin: file,
|
|
1849
|
+
beforeSrc: src,
|
|
1850
|
+
afterSrc: canvasURL,
|
|
1851
|
+
beforeKB: sizeKB,
|
|
1852
|
+
afterKB: Number((miniFile.size / 1024).toFixed(2))
|
|
1853
|
+
});
|
|
1854
|
+
};
|
|
1855
|
+
image.src = src;
|
|
1856
|
+
};
|
|
1857
|
+
reader.readAsDataURL(file);
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return Promise.resolve(null);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/*
|
|
1865
|
+
* @created: Saturday, 2020-04-18 14:38:23
|
|
1866
|
+
* @author: chendq
|
|
1867
|
+
* ---------
|
|
1868
|
+
* @desc 网页加水印的工具类
|
|
1869
|
+
*/
|
|
1870
|
+
/**
|
|
1871
|
+
* canvas 实现 水印, 具备防删除功能
|
|
1872
|
+
* @param {ICanvasWM} canvasWM
|
|
1873
|
+
* @example genCanvasWM({ content: 'QQMusicFE' })
|
|
1874
|
+
*/
|
|
1875
|
+
function genCanvasWM(content = '请勿外传', canvasWM) {
|
|
1876
|
+
const { rootContainer = document.body, width = '300px', height = '150px', textAlign = 'center', textBaseline = 'middle', font = '20px PingFangSC-Medium,PingFang SC',
|
|
1877
|
+
// fontWeight = 500,
|
|
1878
|
+
fillStyle = 'rgba(189, 177, 167, .3)', rotate = -20, zIndex = 2147483647, watermarkId = '__wm' } = isNullOrUnDef(canvasWM) ? {} : canvasWM;
|
|
1879
|
+
const container = isString(rootContainer) ? document.querySelector(rootContainer) : rootContainer;
|
|
1880
|
+
if (!container) {
|
|
1881
|
+
throw new Error(`${rootContainer} is not valid Html Element or element selector`);
|
|
1882
|
+
}
|
|
1883
|
+
const canvas = document.createElement('canvas');
|
|
1884
|
+
canvas.setAttribute('width', width);
|
|
1885
|
+
canvas.setAttribute('height', height);
|
|
1886
|
+
const ctx = canvas.getContext('2d');
|
|
1887
|
+
ctx.textAlign = textAlign;
|
|
1888
|
+
ctx.textBaseline = textBaseline;
|
|
1889
|
+
ctx.font = font;
|
|
1890
|
+
// ctx!.fontWeight = fontWeight;
|
|
1891
|
+
ctx.fillStyle = fillStyle;
|
|
1892
|
+
ctx.rotate((Math.PI / 180) * rotate);
|
|
1893
|
+
ctx.fillText(content, parseFloat(width) / 4, parseFloat(height) / 2);
|
|
1894
|
+
const base64Url = canvas.toDataURL();
|
|
1895
|
+
const __wm = document.querySelector(`#${watermarkId}`);
|
|
1896
|
+
const watermarkDiv = __wm || document.createElement('div');
|
|
1897
|
+
const styleStr = `opacity: 1 !important; display: block !important; visibility: visible !important; position:absolute; left:0; top:0; width:100%; height:100%; z-index:${zIndex}; pointer-events:none; background-repeat:repeat; background-image:url('${base64Url}')`;
|
|
1898
|
+
watermarkDiv.setAttribute('style', styleStr);
|
|
1899
|
+
watermarkDiv.setAttribute('id', watermarkId);
|
|
1900
|
+
watermarkDiv.classList.add('nav-height');
|
|
1901
|
+
if (!__wm) {
|
|
1902
|
+
container.style.position = 'relative';
|
|
1903
|
+
container.appendChild(watermarkDiv);
|
|
1904
|
+
}
|
|
1905
|
+
const getMutableStyle = (ele) => {
|
|
1906
|
+
const computedStyle = getComputedStyle(ele);
|
|
1907
|
+
return {
|
|
1908
|
+
opacity: computedStyle.getPropertyValue('opacity'),
|
|
1909
|
+
zIndex: computedStyle.getPropertyValue('z-index'),
|
|
1910
|
+
display: computedStyle.getPropertyValue('display'),
|
|
1911
|
+
visibility: computedStyle.getPropertyValue('visibility')
|
|
1912
|
+
};
|
|
1913
|
+
};
|
|
1914
|
+
//@ts-ignore
|
|
1915
|
+
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
|
1916
|
+
if (MutationObserver) {
|
|
1917
|
+
let mo = new MutationObserver(function () {
|
|
1918
|
+
let __wm = document.querySelector(`#${watermarkId}`); // 只在__wm元素变动才重新调用 __canvasWM
|
|
1919
|
+
if (!__wm) {
|
|
1920
|
+
// 避免一直触发
|
|
1921
|
+
// console.log('regenerate watermark by delete::')
|
|
1922
|
+
mo.disconnect();
|
|
1923
|
+
mo = null;
|
|
1924
|
+
genCanvasWM(content, canvasWM);
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
const { opacity, zIndex, display, visibility } = getMutableStyle(__wm);
|
|
1928
|
+
if ((__wm && __wm.getAttribute('style') !== styleStr) ||
|
|
1929
|
+
!__wm ||
|
|
1930
|
+
!(opacity === '1' && zIndex === '2147483647' && display === 'block' && visibility === 'visible')) {
|
|
1931
|
+
// 避免一直触发
|
|
1932
|
+
// console.log('regenerate watermark by inline style changed ::')
|
|
1933
|
+
mo.disconnect();
|
|
1934
|
+
mo = null;
|
|
1935
|
+
container.removeChild(__wm);
|
|
1936
|
+
// @ts-ignore
|
|
1937
|
+
__wm = null;
|
|
1938
|
+
genCanvasWM(content, canvasWM);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
mo.observe(container, { attributes: true, subtree: true, childList: true });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* 防抖函数
|
|
1948
|
+
* 当函数被连续调用时,该函数并不执行,只有当其全部停止调用超过一定时间后才执行1次。
|
|
1949
|
+
* 例如:上电梯的时候,大家陆陆续续进来,电梯的门不会关上,只有当一段时间都没有人上来,电梯才会关门。
|
|
1950
|
+
* @param {F} func
|
|
1951
|
+
* @param {number} wait
|
|
1952
|
+
* @returns {DebounceFunc<F>}
|
|
1953
|
+
*/
|
|
1954
|
+
const debounce = (func, wait) => {
|
|
1955
|
+
let timeout;
|
|
1956
|
+
let canceled = false;
|
|
1957
|
+
const f = function (...args) {
|
|
1958
|
+
if (canceled)
|
|
1959
|
+
return;
|
|
1960
|
+
clearTimeout(timeout);
|
|
1961
|
+
timeout = setTimeout(() => {
|
|
1962
|
+
func.call(this, ...args);
|
|
1963
|
+
}, wait);
|
|
1964
|
+
};
|
|
1965
|
+
f.cancel = () => {
|
|
1966
|
+
clearTimeout(timeout);
|
|
1967
|
+
canceled = true;
|
|
1968
|
+
};
|
|
1969
|
+
return f;
|
|
1970
|
+
};
|
|
1971
|
+
/**
|
|
1972
|
+
* 节流函数
|
|
1973
|
+
* 节流就是节约流量,将连续触发的事件稀释成预设评率。 比如每间隔1秒执行一次函数,无论这期间触发多少次事件。
|
|
1974
|
+
* 这有点像公交车,无论在站点等车的人多不多,公交车只会按时来一班,不会来一个人就来一辆公交车。
|
|
1975
|
+
* @param {F} func
|
|
1976
|
+
* @param {number} wait
|
|
1977
|
+
* @param {boolean} immediate
|
|
1978
|
+
* @returns {ThrottleFunc<F>}
|
|
1979
|
+
*/
|
|
1980
|
+
const throttle = (func, wait, immediate) => {
|
|
1981
|
+
let timeout;
|
|
1982
|
+
let canceled = false;
|
|
1983
|
+
let lastCalledTime = 0;
|
|
1984
|
+
const f = function (...args) {
|
|
1985
|
+
if (canceled)
|
|
1986
|
+
return;
|
|
1987
|
+
const now = Date.now();
|
|
1988
|
+
const call = () => {
|
|
1989
|
+
lastCalledTime = now;
|
|
1990
|
+
func.call(this, ...args);
|
|
1991
|
+
};
|
|
1992
|
+
// 第一次执行
|
|
1993
|
+
if (lastCalledTime === 0) {
|
|
1994
|
+
if (immediate) {
|
|
1995
|
+
return call();
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
lastCalledTime = now;
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const remain = lastCalledTime + wait - now;
|
|
2003
|
+
if (remain > 0) {
|
|
2004
|
+
clearTimeout(timeout);
|
|
2005
|
+
timeout = setTimeout(() => call(), wait);
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
call();
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
f.cancel = () => {
|
|
2012
|
+
clearTimeout(timeout);
|
|
2013
|
+
canceled = true;
|
|
2014
|
+
};
|
|
2015
|
+
return f;
|
|
2016
|
+
};
|
|
2017
|
+
/**
|
|
2018
|
+
* 单次函数
|
|
2019
|
+
* @param {AnyFunc} func
|
|
2020
|
+
* @returns {AnyFunc}
|
|
2021
|
+
*/
|
|
2022
|
+
const once = (func) => {
|
|
2023
|
+
let called = false;
|
|
2024
|
+
let result;
|
|
2025
|
+
return function (...args) {
|
|
2026
|
+
if (called)
|
|
2027
|
+
return result;
|
|
2028
|
+
called = true;
|
|
2029
|
+
result = func.call(this, ...args);
|
|
2030
|
+
return result;
|
|
2031
|
+
};
|
|
2032
|
+
};
|
|
2033
|
+
/**
|
|
2034
|
+
* 设置全局变量
|
|
2035
|
+
* @param {string | number | symbol} key
|
|
2036
|
+
* @param val
|
|
2037
|
+
*/
|
|
2038
|
+
function setGlobal(key, val) {
|
|
2039
|
+
if (typeof globalThis !== 'undefined')
|
|
2040
|
+
globalThis[key] = val;
|
|
2041
|
+
else if (typeof window !== 'undefined')
|
|
2042
|
+
window[key] = val;
|
|
2043
|
+
else if (typeof global !== 'undefined')
|
|
2044
|
+
global[key] = val;
|
|
2045
|
+
else if (typeof self !== 'undefined')
|
|
2046
|
+
self[key] = val;
|
|
2047
|
+
else
|
|
2048
|
+
throw new SyntaxError('当前环境下无法设置全局属性');
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* 获取全局变量
|
|
2052
|
+
* @param {string | number | symbol} key
|
|
2053
|
+
* @param val
|
|
2054
|
+
*/
|
|
2055
|
+
function getGlobal(key) {
|
|
2056
|
+
if (typeof globalThis !== 'undefined')
|
|
2057
|
+
return globalThis[key];
|
|
2058
|
+
else if (typeof window !== 'undefined')
|
|
2059
|
+
return window[key];
|
|
2060
|
+
else if (typeof global !== 'undefined')
|
|
2061
|
+
return global[key];
|
|
2062
|
+
else if (typeof self !== 'undefined')
|
|
2063
|
+
return self[key];
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* 随机整数
|
|
2068
|
+
* @param {number} min
|
|
2069
|
+
* @param {number} max
|
|
2070
|
+
* @returns {number}
|
|
2071
|
+
*/
|
|
2072
|
+
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
|
|
2073
|
+
const STRING_POOL = `${STRING_ARABIC_NUMERALS}${STRING_UPPERCASE_ALPHA}${STRING_LOWERCASE_ALPHA}`;
|
|
2074
|
+
/**
|
|
2075
|
+
* 随机字符串
|
|
2076
|
+
* @param {number | string} length
|
|
2077
|
+
* @param {string} pool
|
|
2078
|
+
* @returns {string}
|
|
2079
|
+
*/
|
|
2080
|
+
const randomString = (length, pool) => {
|
|
2081
|
+
let _length = 0;
|
|
2082
|
+
let _pool = STRING_POOL;
|
|
2083
|
+
if (isString(pool)) {
|
|
2084
|
+
_length = length;
|
|
2085
|
+
_pool = pool;
|
|
2086
|
+
}
|
|
2087
|
+
else if (isNumber(length)) {
|
|
2088
|
+
_length = length;
|
|
2089
|
+
}
|
|
2090
|
+
else if (isString(length)) {
|
|
2091
|
+
_pool = length;
|
|
2092
|
+
}
|
|
2093
|
+
let times = Math.max(_length, 1);
|
|
2094
|
+
let result = '';
|
|
2095
|
+
const min = 0;
|
|
2096
|
+
const max = _pool.length - 1;
|
|
2097
|
+
if (max < 2)
|
|
2098
|
+
throw new Error('字符串池长度不能少于 2');
|
|
2099
|
+
while (times--) {
|
|
2100
|
+
const index = randomNumber(min, max);
|
|
2101
|
+
result += _pool[index];
|
|
2102
|
+
}
|
|
2103
|
+
return result;
|
|
2104
|
+
};
|
|
2105
|
+
/**
|
|
2106
|
+
* 优先浏览器原生能力获取 UUID v4
|
|
2107
|
+
* @returns {string}
|
|
2108
|
+
*/
|
|
2109
|
+
function randomUuid() {
|
|
2110
|
+
if (typeof URL === 'undefined' || !URL.createObjectURL || typeof Blob === 'undefined') {
|
|
2111
|
+
const hex = '0123456789abcdef';
|
|
2112
|
+
const model = 'xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx';
|
|
2113
|
+
let str = '';
|
|
2114
|
+
for (let i = 0; i < model.length; i++) {
|
|
2115
|
+
const rnd = randomNumber(0, 15);
|
|
2116
|
+
str += model[i] == '-' || model[i] == '4' ? model[i] : hex[rnd];
|
|
2117
|
+
}
|
|
2118
|
+
return str;
|
|
2119
|
+
}
|
|
2120
|
+
return /[^/]+$/.exec(URL.createObjectURL(new Blob()).slice())[0];
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const HEX_POOL = `${STRING_ARABIC_NUMERALS}${STRING_UPPERCASE_ALPHA}${STRING_LOWERCASE_ALPHA}`;
|
|
2124
|
+
const supportBigInt = typeof BigInt !== 'undefined';
|
|
2125
|
+
const jsbi = () => getGlobal('JSBI');
|
|
2126
|
+
const toBigInt = (n) => (supportBigInt ? BigInt(n) : jsbi().BigInt(n));
|
|
2127
|
+
const divide$1 = (x, y) => (supportBigInt ? x / y : jsbi().divide(x, y));
|
|
2128
|
+
const remainder = (x, y) => (supportBigInt ? x % y : jsbi().remainder(x, y));
|
|
2129
|
+
/**
|
|
2130
|
+
* 将十进制转换成任意进制
|
|
2131
|
+
* @param {number | string} decimal 十进制数值或字符串,可以是任意长度,会使用大数进行计算
|
|
2132
|
+
* @param {string} [hexPool] 进制池,默认 62 进制
|
|
2133
|
+
* @returns {string}
|
|
2134
|
+
*/
|
|
2135
|
+
function numberToHex(decimal, hexPool = HEX_POOL) {
|
|
2136
|
+
if (hexPool.length < 2)
|
|
2137
|
+
throw new Error('进制池长度不能少于 2');
|
|
2138
|
+
if (!supportBigInt) {
|
|
2139
|
+
throw new Error('需要安装 jsbi 模块并将 JSBI 设置为全局变量:\nimport JSBI from "jsbi"; window.JSBI = JSBI;');
|
|
2140
|
+
}
|
|
2141
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
2142
|
+
let bigInt = toBigInt(decimal);
|
|
2143
|
+
const ret = [];
|
|
2144
|
+
const { length } = hexPool;
|
|
2145
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
2146
|
+
const bigLength = toBigInt(length);
|
|
2147
|
+
const execute = () => {
|
|
2148
|
+
const y = Number(remainder(bigInt, bigLength));
|
|
2149
|
+
bigInt = divide$1(bigInt, bigLength);
|
|
2150
|
+
ret.unshift(hexPool[y]);
|
|
2151
|
+
if (bigInt > 0) {
|
|
2152
|
+
execute();
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
execute();
|
|
2156
|
+
return ret.join('');
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* 将数字转换为携带单位的字符串
|
|
2160
|
+
* @param {number | string} num
|
|
2161
|
+
* @param {Array<string>} units
|
|
2162
|
+
* @param {INumberAbbr} options default: { ratio: 1000, decimals: 0, separator: ' ' }
|
|
2163
|
+
* @returns {string}
|
|
2164
|
+
*/
|
|
2165
|
+
const numberAbbr = (num, units, options = { ratio: 1000, decimals: 0, separator: ' ' }) => {
|
|
2166
|
+
const { ratio = 1000, decimals = 0, separator = ' ' } = options;
|
|
2167
|
+
const { length } = units;
|
|
2168
|
+
if (length === 0)
|
|
2169
|
+
throw new Error('At least one unit is required');
|
|
2170
|
+
let num2 = Number(num);
|
|
2171
|
+
let times = 0;
|
|
2172
|
+
while (num2 >= ratio && times < length - 1) {
|
|
2173
|
+
num2 = num2 / ratio;
|
|
2174
|
+
times++;
|
|
2175
|
+
}
|
|
2176
|
+
const value = num2.toFixed(decimals);
|
|
2177
|
+
const unit = units[times];
|
|
2178
|
+
return String(value) + separator + unit;
|
|
2179
|
+
};
|
|
2180
|
+
/**
|
|
2181
|
+
* Converting file size in bytes to human-readable string
|
|
2182
|
+
* reference: https://zh.wikipedia.org/wiki/%E5%8D%83%E5%AD%97%E8%8A%82
|
|
2183
|
+
* @param {number | string} num bytes Number in Bytes
|
|
2184
|
+
* @param {IHumanFileSizeOptions} options default: { decimals = 0, si = false, separator = ' ' }
|
|
2185
|
+
* si: True to use metric (SI) units, aka powers of 1000, the units is
|
|
2186
|
+
* ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'].
|
|
2187
|
+
* False to use binary (IEC), aka powers of 1024, the units is
|
|
2188
|
+
* ['Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
2189
|
+
* @returns
|
|
2190
|
+
*/
|
|
2191
|
+
function humanFileSize(num, options = { decimals: 0, si: false, separator: ' ' }) {
|
|
2192
|
+
const { decimals = 0, si = false, separator = ' ', baseUnit, maxUnit } = options;
|
|
2193
|
+
let units = si
|
|
2194
|
+
? ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
2195
|
+
: ['Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
2196
|
+
if (!isNullOrUnDef(baseUnit)) {
|
|
2197
|
+
const targetIndex = units.findIndex(el => el === baseUnit);
|
|
2198
|
+
if (targetIndex !== -1) {
|
|
2199
|
+
units = units.slice(targetIndex);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (!isNullOrUnDef(maxUnit)) {
|
|
2203
|
+
const targetIndex = units.findIndex(el => el === maxUnit);
|
|
2204
|
+
if (targetIndex !== -1) {
|
|
2205
|
+
units.splice(targetIndex + 1);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return numberAbbr(num, units, { ratio: si ? 1000 : 1024, decimals, separator });
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* 将数字格式化成千位分隔符显示的字符串
|
|
2212
|
+
* @param {number|string} num 数字
|
|
2213
|
+
* @param {number} decimals 格式化成指定小数位精度的参数
|
|
2214
|
+
* @returns {string}
|
|
2215
|
+
*/
|
|
2216
|
+
function formatNumber(num, decimals) {
|
|
2217
|
+
if (isNullOrUnDef(decimals)) {
|
|
2218
|
+
return parseInt(String(num)).toLocaleString();
|
|
2219
|
+
}
|
|
2220
|
+
let prec = 0;
|
|
2221
|
+
if (!isNumber(decimals)) {
|
|
2222
|
+
throw new Error('Decimals must be a positive number not less than zero');
|
|
2223
|
+
}
|
|
2224
|
+
else if (decimals > 0) {
|
|
2225
|
+
prec = decimals;
|
|
2226
|
+
}
|
|
2227
|
+
return Number(Number(num).toFixed(prec)).toLocaleString('en-US');
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const padStartWithZero = (str, maxLength = 2) => String(str).padStart(maxLength, '0');
|
|
2231
|
+
let safeNo = 0;
|
|
2232
|
+
let lastTimestamp = 0;
|
|
2233
|
+
// 安全后缀长度,按 1 毫秒运算 99999 次 JS 代码来进行估算
|
|
2234
|
+
// 那么,补足长度为 5
|
|
2235
|
+
// 时间戳模式长度为 13
|
|
2236
|
+
// 取最长 5 + 13 = 18
|
|
2237
|
+
const UNIQUE_NUMBER_SAFE_LENGTH = 18;
|
|
2238
|
+
const FIX_SAFE_LENGTH = 5;
|
|
2239
|
+
const TIMESTAMP_LENGTH = 13;
|
|
2240
|
+
/**
|
|
2241
|
+
* 生成唯一不重复数值
|
|
2242
|
+
* @param {number} length
|
|
2243
|
+
* @returns {string}
|
|
2244
|
+
*/
|
|
2245
|
+
const uniqueNumber = (length = UNIQUE_NUMBER_SAFE_LENGTH) => {
|
|
2246
|
+
const now = Date.now();
|
|
2247
|
+
length = Math.max(length, UNIQUE_NUMBER_SAFE_LENGTH);
|
|
2248
|
+
if (now !== lastTimestamp) {
|
|
2249
|
+
lastTimestamp = now;
|
|
2250
|
+
safeNo = 0;
|
|
2251
|
+
}
|
|
2252
|
+
const timestamp = `${now}`;
|
|
2253
|
+
let random = '';
|
|
2254
|
+
const rndLength = length - FIX_SAFE_LENGTH - TIMESTAMP_LENGTH;
|
|
2255
|
+
if (rndLength > 0) {
|
|
2256
|
+
const rndMin = 10 ** (rndLength - 1);
|
|
2257
|
+
const rndMax = 10 ** rndLength - 1;
|
|
2258
|
+
const rnd = randomNumber(rndMin, rndMax);
|
|
2259
|
+
random = `${rnd}`;
|
|
2260
|
+
}
|
|
2261
|
+
const safe = padStartWithZero(safeNo, FIX_SAFE_LENGTH);
|
|
2262
|
+
safeNo++;
|
|
2263
|
+
return `${timestamp}${random}${safe}`;
|
|
2264
|
+
};
|
|
2265
|
+
const randomFromPool = (pool) => {
|
|
2266
|
+
const poolIndex = randomNumber(0, pool.length - 1);
|
|
2267
|
+
return pool[poolIndex];
|
|
2268
|
+
};
|
|
2269
|
+
/**
|
|
2270
|
+
* 生成唯一不重复字符串
|
|
2271
|
+
* @param {number | string} length
|
|
2272
|
+
* @param {string} pool
|
|
2273
|
+
* @returns {string}
|
|
2274
|
+
*/
|
|
2275
|
+
const uniqueString = (length, pool) => {
|
|
2276
|
+
let _length = 0;
|
|
2277
|
+
let _pool = HEX_POOL;
|
|
2278
|
+
if (isString(pool)) {
|
|
2279
|
+
_length = length;
|
|
2280
|
+
_pool = pool;
|
|
2281
|
+
}
|
|
2282
|
+
else if (isNumber(length)) {
|
|
2283
|
+
_length = length;
|
|
2284
|
+
}
|
|
2285
|
+
else if (isString(length)) {
|
|
2286
|
+
_pool = length;
|
|
2287
|
+
}
|
|
2288
|
+
let uniqueString = numberToHex(uniqueNumber(), _pool);
|
|
2289
|
+
let insertLength = _length - uniqueString.length;
|
|
2290
|
+
if (insertLength <= 0)
|
|
2291
|
+
return uniqueString;
|
|
2292
|
+
while (insertLength--) {
|
|
2293
|
+
uniqueString += randomFromPool(_pool);
|
|
2294
|
+
}
|
|
2295
|
+
return uniqueString;
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* 自定义的 tooltip, 支持鼠标移动动悬浮提示
|
|
2300
|
+
* @Desc 自定义的tooltip方法, 支持拖动悬浮提示
|
|
2301
|
+
* Created by chendeqiao on 2017/5/8.
|
|
2302
|
+
* @example
|
|
2303
|
+
* <span onmouseleave="handleMouseLeave('#root')"
|
|
2304
|
+
* onmousemove="handleMouseEnter({rootContainer: '#root', title: 'title content', event: event})"
|
|
2305
|
+
* onmouseenter="handleMouseEnter({rootContainer:'#root', title: 'title content', event: event})">
|
|
2306
|
+
* title content
|
|
2307
|
+
* </span>
|
|
2308
|
+
*/
|
|
2309
|
+
/**
|
|
2310
|
+
* 自定义title提示功能的mouseenter事件句柄
|
|
2311
|
+
* @param {ITooltipParams} param
|
|
2312
|
+
* @returns {*}
|
|
2313
|
+
*/
|
|
2314
|
+
function handleMouseEnter({ rootContainer = '#root', title, event, bgColor = '#000', color = '#fff' }) {
|
|
2315
|
+
try {
|
|
2316
|
+
const $rootEl = isString(rootContainer) ? document.querySelector(rootContainer) : rootContainer;
|
|
2317
|
+
if (!$rootEl) {
|
|
2318
|
+
throw new Error(`${rootContainer} is not valid Html Element or element selector`);
|
|
2319
|
+
}
|
|
2320
|
+
let $customTitle = null;
|
|
2321
|
+
const styleId = 'style-tooltip-inner1494304949567';
|
|
2322
|
+
// 动态创建class样式,并加入到head中
|
|
2323
|
+
if (!document.querySelector(`#${styleId}`)) {
|
|
2324
|
+
const tooltipWrapperClass = document.createElement('style');
|
|
2325
|
+
tooltipWrapperClass.type = 'text/css';
|
|
2326
|
+
tooltipWrapperClass.id = styleId;
|
|
2327
|
+
tooltipWrapperClass.innerHTML = `
|
|
2328
|
+
.tooltip-inner1494304949567 {
|
|
2329
|
+
max-width: 250px;
|
|
2330
|
+
padding: 3px 8px;
|
|
2331
|
+
color: ${color};
|
|
2332
|
+
text-decoration: none;
|
|
2333
|
+
border-radius: 4px;
|
|
2334
|
+
text-align: left;
|
|
2335
|
+
background-color: ${bgColor};
|
|
2336
|
+
}
|
|
2337
|
+
`;
|
|
2338
|
+
document.querySelector('head').appendChild(tooltipWrapperClass);
|
|
2339
|
+
}
|
|
2340
|
+
$customTitle = document.querySelector('#customTitle1494304949567');
|
|
2341
|
+
if ($customTitle) {
|
|
2342
|
+
mouseenter($customTitle, title, event);
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
const $contentContainer = document.createElement('div');
|
|
2346
|
+
$contentContainer.id = 'customTitle1494304949567';
|
|
2347
|
+
$contentContainer.style.cssText = 'z-index: 99999999; visibility: hidden; position: absolute;';
|
|
2348
|
+
$contentContainer.innerHTML =
|
|
2349
|
+
'<div class="tooltip-inner1494304949567" style="word-wrap: break-word; max-width: 44px;">皮肤</div>';
|
|
2350
|
+
$rootEl.appendChild($contentContainer);
|
|
2351
|
+
$customTitle = document.querySelector('#customTitle1494304949567');
|
|
2352
|
+
if (title) {
|
|
2353
|
+
//判断div显示的内容是否为空
|
|
2354
|
+
mouseenter($customTitle, title, event);
|
|
2355
|
+
$customTitle.style.visibility = 'visible';
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
catch (e) {
|
|
2360
|
+
console.error(e.message);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* 提示文案dom渲染的处理函数
|
|
2365
|
+
* @param {HTMLDivElement} customTitle
|
|
2366
|
+
* @param {string} title 提示的字符串
|
|
2367
|
+
* @param {PointerEvent} e 事件对象
|
|
2368
|
+
* @returns {*}
|
|
2369
|
+
*/
|
|
2370
|
+
function mouseenter($customTitle, title, e) {
|
|
2371
|
+
let diffValueX = 200 + 50; //默认设置弹出div的宽度为250px
|
|
2372
|
+
let x = 13;
|
|
2373
|
+
const y = 23;
|
|
2374
|
+
const $contentEle = $customTitle.children[0];
|
|
2375
|
+
if (getStrWidthPx(title, 12) < 180 + 50) {
|
|
2376
|
+
//【弹出div自适应字符串宽度】若显示的字符串占用宽度小于180,则设置弹出div的宽度为“符串占用宽度”+20
|
|
2377
|
+
$contentEle.style.maxWidth = getStrWidthPx(title, 12) + 20 + 50 + 'px';
|
|
2378
|
+
diffValueX = e.clientX + (getStrWidthPx(title, 12) + 50) - document.body.offsetWidth;
|
|
2379
|
+
}
|
|
2380
|
+
else {
|
|
2381
|
+
$contentEle.style.maxWidth = '250px';
|
|
2382
|
+
diffValueX = e.clientX + 230 - document.body.offsetWidth; //计算div水平方向显示的内容超出屏幕多少宽度
|
|
2383
|
+
}
|
|
2384
|
+
$contentEle.innerHTML = title; //html方法可解析内容中换行标签,text方法不能
|
|
2385
|
+
if (diffValueX > 0) {
|
|
2386
|
+
//水平方向超出可见区域时
|
|
2387
|
+
x -= diffValueX;
|
|
2388
|
+
}
|
|
2389
|
+
$customTitle.style.top = e.clientY + y + 'px';
|
|
2390
|
+
$customTitle.style.left = e.clientX + x + 'px';
|
|
2391
|
+
$customTitle.style.maxWidth = '250px';
|
|
2392
|
+
const diffValueY = $customTitle.getBoundingClientRect().top + $contentEle.offsetHeight - document.body.offsetHeight;
|
|
2393
|
+
if (diffValueY > 0) {
|
|
2394
|
+
//垂直方向超出可见区域时
|
|
2395
|
+
$customTitle.style.top = e.clientY - diffValueY + 'px';
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* 移除提示文案dom的事件句柄
|
|
2400
|
+
* @param {string} rootContainer
|
|
2401
|
+
* @returns {*}
|
|
2402
|
+
*/
|
|
2403
|
+
function handleMouseLeave(rootContainer = '#root') {
|
|
2404
|
+
const rootEl = isString(rootContainer) ? document.querySelector(rootContainer) : rootContainer;
|
|
2405
|
+
let titleEl = document.querySelector('#customTitle1494304949567');
|
|
2406
|
+
if (rootEl && titleEl) {
|
|
2407
|
+
rootEl.removeChild(titleEl);
|
|
2408
|
+
// @ts-ignore
|
|
2409
|
+
titleEl = null;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
const tooltipEvent = { handleMouseEnter, handleMouseLeave };
|
|
2413
|
+
|
|
2414
|
+
const defaultFieldOptions = { keyField: 'key', childField: 'children', pidField: 'pid' };
|
|
2415
|
+
const defaultSearchTreeOptions = {
|
|
2416
|
+
childField: 'children',
|
|
2417
|
+
nameField: 'name',
|
|
2418
|
+
removeEmptyChild: false,
|
|
2419
|
+
ignoreCase: true
|
|
2420
|
+
};
|
|
2421
|
+
/**
|
|
2422
|
+
* 树遍历函数(支持continue和break操作), 可用于遍历Array和NodeList类型的数据
|
|
2423
|
+
* @param {ArrayLike<V>} tree 树形数据
|
|
2424
|
+
* @param {Function} iterator 迭代函数, 返回值为true时continue, 返回值为false时break
|
|
2425
|
+
* @param {options} options 支持定制子元素名称、反向遍历、广度优先遍历,默认{
|
|
2426
|
+
childField: 'children',
|
|
2427
|
+
reverse: false,
|
|
2428
|
+
breadthFirst: false
|
|
2429
|
+
}
|
|
2430
|
+
* @returns {*}
|
|
2431
|
+
*/
|
|
2432
|
+
function forEachDeep(tree, iterator, options = {
|
|
2433
|
+
childField: 'children',
|
|
2434
|
+
reverse: false,
|
|
2435
|
+
breadthFirst: false,
|
|
2436
|
+
isDomNode: false
|
|
2437
|
+
}) {
|
|
2438
|
+
const { childField = 'children', reverse = false, breadthFirst = false, isDomNode = false } = isObject(options) ? options : {};
|
|
2439
|
+
let isBreak = false;
|
|
2440
|
+
const queue = [];
|
|
2441
|
+
const walk = (arr, parent, level = 0) => {
|
|
2442
|
+
if (reverse) {
|
|
2443
|
+
for (let index = arr.length - 1; index >= 0; index--) {
|
|
2444
|
+
if (isBreak) {
|
|
2445
|
+
break;
|
|
2446
|
+
}
|
|
2447
|
+
const item = arr[index];
|
|
2448
|
+
// 广度优先
|
|
2449
|
+
if (breadthFirst) {
|
|
2450
|
+
queue.push({ item, index, array: arr, tree, parent, level });
|
|
2451
|
+
}
|
|
2452
|
+
else {
|
|
2453
|
+
const re = iterator(item, index, arr, tree, parent, level);
|
|
2454
|
+
if (re === false) {
|
|
2455
|
+
isBreak = true;
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
else if (re === true) {
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if (item && (isDomNode ? isNodeList(item[childField]) : Array.isArray(item[childField]))) {
|
|
2462
|
+
walk(item[childField], item, level + 1);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (breadthFirst) {
|
|
2467
|
+
// Process queue
|
|
2468
|
+
while (queue.length > 0 && !isBreak) {
|
|
2469
|
+
const current = queue.shift();
|
|
2470
|
+
const { item, index, array, tree, parent, level } = current;
|
|
2471
|
+
const re = iterator(item, index, array, tree, parent, level);
|
|
2472
|
+
if (re === false) {
|
|
2473
|
+
isBreak = true;
|
|
2474
|
+
break;
|
|
2475
|
+
}
|
|
2476
|
+
else if (re === true) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (item && (isDomNode ? isNodeList(item[childField]) : Array.isArray(item[childField]))) {
|
|
2480
|
+
walk(item[childField], item, level + 1);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
else {
|
|
2486
|
+
for (let index = 0, len = arr.length; index < len; index++) {
|
|
2487
|
+
if (isBreak) {
|
|
2488
|
+
break;
|
|
2489
|
+
}
|
|
2490
|
+
const item = arr[index];
|
|
2491
|
+
if (breadthFirst) {
|
|
2492
|
+
// 广度优先
|
|
2493
|
+
queue.push({ item, index: index, array: arr, tree, parent, level });
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
// 深度优先
|
|
2497
|
+
const re = iterator(item, index, arr, tree, parent, level);
|
|
2498
|
+
if (re === false) {
|
|
2499
|
+
isBreak = true;
|
|
2500
|
+
break;
|
|
2501
|
+
}
|
|
2502
|
+
else if (re === true) {
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
if (item && (isDomNode ? isNodeList(item[childField]) : Array.isArray(item[childField]))) {
|
|
2506
|
+
walk(item[childField], item, level + 1);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (breadthFirst) {
|
|
2511
|
+
while (queue.length > 0 && !isBreak) {
|
|
2512
|
+
const current = queue.shift();
|
|
2513
|
+
if (!current)
|
|
2514
|
+
break;
|
|
2515
|
+
const { item, index, array, tree, parent, level } = current;
|
|
2516
|
+
const re = iterator(item, index, array, tree, parent, level);
|
|
2517
|
+
if (re === false) {
|
|
2518
|
+
isBreak = true;
|
|
2519
|
+
break;
|
|
2520
|
+
}
|
|
2521
|
+
else if (re === true) {
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (item && (isDomNode ? isNodeList(item[childField]) : Array.isArray(item[childField]))) {
|
|
2525
|
+
walk(item[childField], item, level + 1);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
walk(tree, null, 0);
|
|
2532
|
+
// @ts-ignore
|
|
2533
|
+
tree = null;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* 创建一个新数组, 深度优先遍历的Map函数(支持continue和break操作), 可用于insert tree item 和 remove tree item
|
|
2537
|
+
*
|
|
2538
|
+
* 可遍历任何带有 length 属性和数字键的类数组对象
|
|
2539
|
+
* @param {ArrayLike<V>} tree 树形数据
|
|
2540
|
+
* @param {Function} iterator 迭代函数, 返回值为true时continue, 返回值为false时break
|
|
2541
|
+
* @param {options} options 支持定制子元素名称、反向遍历,默认{
|
|
2542
|
+
childField: 'children',
|
|
2543
|
+
reverse: false,
|
|
2544
|
+
}
|
|
2545
|
+
* @returns {any[]} 新的一棵树
|
|
2546
|
+
*/
|
|
2547
|
+
function mapDeep(tree, iterator, options = {
|
|
2548
|
+
childField: 'children',
|
|
2549
|
+
reverse: false
|
|
2550
|
+
}) {
|
|
2551
|
+
const { childField = 'children', reverse = false } = isObject(options) ? options : {};
|
|
2552
|
+
let isBreak = false;
|
|
2553
|
+
const newTree = [];
|
|
2554
|
+
const walk = (arr, parent, newTree, level = 0) => {
|
|
2555
|
+
if (reverse) {
|
|
2556
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
2557
|
+
if (isBreak) {
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
const item = arr[i];
|
|
2561
|
+
const re = iterator(item, i, arr, tree, parent, level);
|
|
2562
|
+
if (re === false) {
|
|
2563
|
+
isBreak = true;
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
else if (re === true) {
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
newTree.push(objectOmit(re, [childField]));
|
|
2570
|
+
if (item && Array.isArray(item[childField])) {
|
|
2571
|
+
newTree[newTree.length - 1][childField] = [];
|
|
2572
|
+
walk(item[childField], item, newTree[newTree.length - 1][childField], level + 1);
|
|
2573
|
+
}
|
|
2574
|
+
else {
|
|
2575
|
+
// children非有效数组时,移除该属性字段
|
|
2576
|
+
delete re[childField];
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2582
|
+
if (isBreak) {
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
const item = arr[i];
|
|
2586
|
+
const re = iterator(item, i, arr, tree, parent, level);
|
|
2587
|
+
if (re === false) {
|
|
2588
|
+
isBreak = true;
|
|
2589
|
+
break;
|
|
2590
|
+
}
|
|
2591
|
+
else if (re === true) {
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
newTree.push(objectOmit(re, [childField]));
|
|
2595
|
+
if (item && Array.isArray(item[childField])) {
|
|
2596
|
+
newTree[newTree.length - 1][childField] = [];
|
|
2597
|
+
walk(item[childField], item, newTree[newTree.length - 1][childField], level + 1);
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
// children非有效数组时,移除该属性字段
|
|
2601
|
+
delete re[childField];
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
walk(tree, null, newTree);
|
|
2607
|
+
// @ts-ignore
|
|
2608
|
+
tree = null;
|
|
2609
|
+
return newTree;
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* 在树中找到 id 为某个值的节点,并返回上游的所有父级节点
|
|
2613
|
+
*
|
|
2614
|
+
* @param {ArrayLike<T>} tree - 树形数据
|
|
2615
|
+
* @param {number | string} nodeId - 目标元素ID
|
|
2616
|
+
* @param {ITreeConf} options - 迭代配置项, 默认:{ children = 'children', id = 'id' }
|
|
2617
|
+
* @returns {[(number | string)[], V[]]} - 由parentId...childId, parentObject-childObject组成的二维数组
|
|
2618
|
+
*/
|
|
2619
|
+
function searchTreeById(tree, nodeId, options = { childField: 'children', keyField: 'id' }) {
|
|
2620
|
+
const { childField = 'children', keyField = 'id' } = isObject(options) ? options : {};
|
|
2621
|
+
const toFlatArray = (tree, parentId, parent) => {
|
|
2622
|
+
return tree.reduce((t, _) => {
|
|
2623
|
+
const child = _[childField];
|
|
2624
|
+
return [
|
|
2625
|
+
...t,
|
|
2626
|
+
parentId ? { ..._, parentId, parent } : _,
|
|
2627
|
+
...(child && child.length ? toFlatArray(child, _[keyField], _) : [])
|
|
2628
|
+
];
|
|
2629
|
+
}, []);
|
|
2630
|
+
};
|
|
2631
|
+
const getIds = (flatArray) => {
|
|
2632
|
+
let child = flatArray.find(_ => _[keyField] === nodeId);
|
|
2633
|
+
const { parent, parentId, ...other } = child;
|
|
2634
|
+
let ids = [nodeId], nodes = [other];
|
|
2635
|
+
while (child && child.parentId) {
|
|
2636
|
+
ids = [child.parentId, ...ids];
|
|
2637
|
+
nodes = [child.parent, ...nodes];
|
|
2638
|
+
child = flatArray.find(_ => _[keyField] === child.parentId); // eslint-disable-line
|
|
2639
|
+
}
|
|
2640
|
+
return [ids, nodes];
|
|
2641
|
+
};
|
|
2642
|
+
return getIds(toFlatArray(tree));
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* 扁平化数组转换成树
|
|
2646
|
+
* @param {any[]} list
|
|
2647
|
+
* @param {IFieldOptions} options 定制id字段名,子元素字段名,父元素字段名,默认
|
|
2648
|
+
* { keyField: 'key', childField: 'children', pidField: 'pid' }
|
|
2649
|
+
* @returns {any[]}
|
|
2650
|
+
*/
|
|
2651
|
+
function formatTree(list, options = defaultFieldOptions) {
|
|
2652
|
+
const { keyField = 'key', childField = 'children', pidField = 'pid' } = isObject(options) ? options : {};
|
|
2653
|
+
const treeArr = [];
|
|
2654
|
+
const sourceMap = {};
|
|
2655
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
2656
|
+
const item = list[i];
|
|
2657
|
+
sourceMap[item[keyField]] = item;
|
|
2658
|
+
}
|
|
2659
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
2660
|
+
const item = list[i];
|
|
2661
|
+
const parent = sourceMap[item[pidField]];
|
|
2662
|
+
if (parent) {
|
|
2663
|
+
(parent[childField] || (parent[childField] = [])).push(item);
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
treeArr.push(item);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
// @ts-ignore
|
|
2670
|
+
list = null;
|
|
2671
|
+
return treeArr;
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* 树形结构转扁平化
|
|
2675
|
+
* @param {any[]} treeList
|
|
2676
|
+
* @param {IFieldOptions} options 定制id字段名,子元素字段名,父元素字段名,默认
|
|
2677
|
+
* { keyField: 'key', childField: 'children', pidField: 'pid' }
|
|
2678
|
+
* @returns {any[]}
|
|
2679
|
+
*/
|
|
2680
|
+
function flatTree(treeList, options = defaultFieldOptions) {
|
|
2681
|
+
const { keyField = 'key', childField = 'children', pidField = 'pid' } = isObject(options) ? options : {};
|
|
2682
|
+
let res = [];
|
|
2683
|
+
for (let i = 0, len = treeList.length; i < len; i++) {
|
|
2684
|
+
const node = treeList[i];
|
|
2685
|
+
const item = {
|
|
2686
|
+
...node,
|
|
2687
|
+
[childField]: [] // 清空子级
|
|
2688
|
+
};
|
|
2689
|
+
objectHas(item, childField) && delete item[childField];
|
|
2690
|
+
res.push(item);
|
|
2691
|
+
if (node[childField]) {
|
|
2692
|
+
const children = node[childField].map(item => ({
|
|
2693
|
+
...item,
|
|
2694
|
+
[pidField]: node[keyField] || item.pid // 给子级设置pid
|
|
2695
|
+
}));
|
|
2696
|
+
res = res.concat(flatTree(children, options));
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return res;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* 模糊搜索函数,返回包含搜索字符的节点及其祖先节点, 适用于树型组件的字符过滤功能
|
|
2703
|
+
* 以下搜索条件二选一,按先后优先级处理:
|
|
2704
|
+
* 1. 过滤函数filter, 返回true/false
|
|
2705
|
+
* 2. 匹配关键词,支持是否启用忽略大小写来判断
|
|
2706
|
+
*
|
|
2707
|
+
* 有以下特性:
|
|
2708
|
+
* 1. 可配置removeEmptyChild字段,来决定是否移除搜索结果中的空children字段
|
|
2709
|
+
* 2. 若无任何过滤条件或keyword模式匹配且keyword为空串,返回原对象;其他情况返回新数组
|
|
2710
|
+
* @param {V[]} nodes
|
|
2711
|
+
* @param {IFilterCondition} filterCondition
|
|
2712
|
+
* @param {ISearchTreeOpts} options 默认配置项 {
|
|
2713
|
+
childField: 'children',
|
|
2714
|
+
nameField: 'name',
|
|
2715
|
+
removeEmptyChild: false,
|
|
2716
|
+
ignoreCase: true
|
|
2717
|
+
}
|
|
2718
|
+
* @returns {V[]}
|
|
2719
|
+
*/
|
|
2720
|
+
function fuzzySearchTree(nodes, filterCondition, options = defaultSearchTreeOptions) {
|
|
2721
|
+
if (!objectHas(filterCondition, 'filter') &&
|
|
2722
|
+
(!objectHas(filterCondition, 'keyword') || isEmpty(filterCondition.keyword))) {
|
|
2723
|
+
return nodes;
|
|
2724
|
+
}
|
|
2725
|
+
const result = [];
|
|
2726
|
+
for (let i = 0, len = nodes.length; i < len; i++) {
|
|
2727
|
+
const node = nodes[i];
|
|
2728
|
+
// 递归检查子节点是否匹配
|
|
2729
|
+
const matchedChildren = node[options.childField] && node[options.childField].length > 0
|
|
2730
|
+
? fuzzySearchTree(node[options.childField] || [], filterCondition, options)
|
|
2731
|
+
: [];
|
|
2732
|
+
// 检查当前节点是否匹配或者有匹配的子节点
|
|
2733
|
+
if ((objectHas(filterCondition, 'filter')
|
|
2734
|
+
? filterCondition.filter(node)
|
|
2735
|
+
: !options.ignoreCase
|
|
2736
|
+
? node[options.nameField].includes(filterCondition.keyword)
|
|
2737
|
+
: node[options.nameField].toLowerCase().includes(filterCondition.keyword.toLowerCase())) ||
|
|
2738
|
+
matchedChildren.length > 0) {
|
|
2739
|
+
// 将当前节点加入结果中
|
|
2740
|
+
if (node[options.childField]) {
|
|
2741
|
+
if (matchedChildren.length > 0) {
|
|
2742
|
+
result.push({
|
|
2743
|
+
...node,
|
|
2744
|
+
[options.childField]: matchedChildren // 包含匹配的子节点
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
else if (options.removeEmptyChild) {
|
|
2748
|
+
const { [options.childField]: _, ...other } = node;
|
|
2749
|
+
result.push(other);
|
|
2750
|
+
}
|
|
2751
|
+
else {
|
|
2752
|
+
result.push({
|
|
2753
|
+
...node,
|
|
2754
|
+
[options.childField]: []
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
else {
|
|
2759
|
+
const { [options.childField]: _, ...other } = node;
|
|
2760
|
+
result.push(other);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
return result;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/**
|
|
2768
|
+
* 数值安全乘法
|
|
2769
|
+
* @param arg1 数值1
|
|
2770
|
+
* @param arg2 数值2
|
|
2771
|
+
*/
|
|
2772
|
+
const multiply = (arg1, arg2) => {
|
|
2773
|
+
let m = 0;
|
|
2774
|
+
const s1 = arg1.toString();
|
|
2775
|
+
const s2 = arg2.toString();
|
|
2776
|
+
if (s1.split('.')[1] !== undefined)
|
|
2777
|
+
m += s1.split('.')[1].length;
|
|
2778
|
+
if (s2.split('.')[1] !== undefined)
|
|
2779
|
+
m += s2.split('.')[1].length;
|
|
2780
|
+
return (Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) / Math.pow(10, m);
|
|
2781
|
+
};
|
|
2782
|
+
/**
|
|
2783
|
+
* 数值安全加法
|
|
2784
|
+
* @param arg1 数值1
|
|
2785
|
+
* @param arg2 数值2
|
|
2786
|
+
*/
|
|
2787
|
+
const add = (arg1, arg2) => {
|
|
2788
|
+
let r1 = 0;
|
|
2789
|
+
let r2 = 0;
|
|
2790
|
+
let m = 0;
|
|
2791
|
+
try {
|
|
2792
|
+
r1 = arg1.toString().split('.')[1].length;
|
|
2793
|
+
}
|
|
2794
|
+
catch (e) {
|
|
2795
|
+
r1 = 0;
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
r2 = arg2.toString().split('.')[1].length;
|
|
2799
|
+
}
|
|
2800
|
+
catch (e) {
|
|
2801
|
+
r2 = 0;
|
|
2802
|
+
}
|
|
2803
|
+
m = 10 ** Math.max(r1, r2);
|
|
2804
|
+
return (multiply(arg1, m) + multiply(arg2, m)) / m;
|
|
2805
|
+
};
|
|
2806
|
+
/**
|
|
2807
|
+
* 数值安全减法
|
|
2808
|
+
* @param arg1 数值1
|
|
2809
|
+
* @param arg2 数值2
|
|
2810
|
+
*/
|
|
2811
|
+
const subtract = (arg1, arg2) => add(arg1, -arg2);
|
|
2812
|
+
/**
|
|
2813
|
+
* 数值安全除法
|
|
2814
|
+
* @param arg1 数值1
|
|
2815
|
+
* @param arg2 数值2
|
|
2816
|
+
*/
|
|
2817
|
+
const divide = (arg1, arg2) => {
|
|
2818
|
+
let t1 = 0;
|
|
2819
|
+
let t2 = 0;
|
|
2820
|
+
let r1 = 0;
|
|
2821
|
+
let r2 = 0;
|
|
2822
|
+
if (arg1.toString().split('.')[1] !== undefined)
|
|
2823
|
+
t1 = arg1.toString().split('.')[1].length;
|
|
2824
|
+
if (arg2.toString().split('.')[1] !== undefined)
|
|
2825
|
+
t2 = arg2.toString().split('.')[1].length;
|
|
2826
|
+
r1 = Number(arg1.toString().replace('.', ''));
|
|
2827
|
+
r2 = Number(arg2.toString().replace('.', ''));
|
|
2828
|
+
return (r1 / r2) * Math.pow(10, t2 - t1);
|
|
2829
|
+
};
|
|
2830
|
+
/**
|
|
2831
|
+
* Correct the given number to specifying significant digits.
|
|
2832
|
+
*
|
|
2833
|
+
* @param num The input number
|
|
2834
|
+
* @param precision An integer specifying the number of significant digits
|
|
2835
|
+
*
|
|
2836
|
+
* @example strip(0.09999999999999998) === 0.1 // true
|
|
2837
|
+
*/
|
|
2838
|
+
function strip(num, precision = 15) {
|
|
2839
|
+
return +parseFloat(Number(num).toPrecision(precision));
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
2843
|
+
// eslint-disable-next-line
|
|
2844
|
+
const b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
|
|
2845
|
+
/**
|
|
2846
|
+
* 字符串编码成Base64, 平替浏览器的btoa, 不包含中文的处理 (适用于任何环境,包括小程序)
|
|
2847
|
+
* @param {string} string
|
|
2848
|
+
* @returns {string}
|
|
2849
|
+
*/
|
|
2850
|
+
function weBtoa(string) {
|
|
2851
|
+
// 同window.btoa: 字符串编码成Base64
|
|
2852
|
+
string = String(string);
|
|
2853
|
+
let bitmap, a, b, c, result = '', i = 0;
|
|
2854
|
+
const strLen = string.length;
|
|
2855
|
+
const rest = strLen % 3;
|
|
2856
|
+
for (; i < strLen;) {
|
|
2857
|
+
if ((a = string.charCodeAt(i++)) > 255 || (b = string.charCodeAt(i++)) > 255 || (c = string.charCodeAt(i++)) > 255)
|
|
2858
|
+
throw new TypeError("Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.");
|
|
2859
|
+
bitmap = (a << 16) | (b << 8) | c;
|
|
2860
|
+
result +=
|
|
2861
|
+
b64.charAt((bitmap >> 18) & 63) +
|
|
2862
|
+
b64.charAt((bitmap >> 12) & 63) +
|
|
2863
|
+
b64.charAt((bitmap >> 6) & 63) +
|
|
2864
|
+
b64.charAt(bitmap & 63);
|
|
2865
|
+
}
|
|
2866
|
+
return rest ? result.slice(0, rest - 3) + '==='.substring(rest) : result;
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Base64解码为原始字符串,平替浏览器的atob, 不包含中文的处理(适用于任何环境,包括小程序)
|
|
2870
|
+
* @param {string} string
|
|
2871
|
+
* @returns {string}
|
|
2872
|
+
*/
|
|
2873
|
+
function weAtob(string) {
|
|
2874
|
+
// 同window.atob: Base64解码为原始字符串
|
|
2875
|
+
string = String(string).replace(/[\t\n\f\r ]+/g, '');
|
|
2876
|
+
if (!b64re.test(string))
|
|
2877
|
+
throw new TypeError("Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.");
|
|
2878
|
+
string += '=='.slice(2 - (string.length & 3));
|
|
2879
|
+
let bitmap, result = '', r1, r2, i = 0;
|
|
2880
|
+
for (const strLen = string.length; i < strLen;) {
|
|
2881
|
+
bitmap =
|
|
2882
|
+
(b64.indexOf(string.charAt(i++)) << 18) |
|
|
2883
|
+
(b64.indexOf(string.charAt(i++)) << 12) |
|
|
2884
|
+
((r1 = b64.indexOf(string.charAt(i++))) << 6) |
|
|
2885
|
+
(r2 = b64.indexOf(string.charAt(i++)));
|
|
2886
|
+
result +=
|
|
2887
|
+
r1 === 64
|
|
2888
|
+
? String.fromCharCode((bitmap >> 16) & 255)
|
|
2889
|
+
: r2 === 64
|
|
2890
|
+
? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
|
|
2891
|
+
: String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255, bitmap & 255);
|
|
2892
|
+
}
|
|
2893
|
+
return result;
|
|
2894
|
+
}
|
|
2895
|
+
function stringToUint8Array(str) {
|
|
2896
|
+
const utf8 = encodeURIComponent(str); // 将字符串转换为 UTF-8 编码
|
|
2897
|
+
const uint8Array = new Uint8Array(utf8.length); // 创建 Uint8Array
|
|
2898
|
+
for (let i = 0; i < utf8.length; i++) {
|
|
2899
|
+
uint8Array[i] = utf8.charCodeAt(i); // 填充 Uint8Array
|
|
2900
|
+
}
|
|
2901
|
+
return uint8Array;
|
|
2902
|
+
}
|
|
2903
|
+
function uint8ArrayToString(uint8Array) {
|
|
2904
|
+
const utf8 = String.fromCharCode.apply(null, uint8Array); // 将 Uint8Array 转为字符串
|
|
2905
|
+
return decodeURIComponent(utf8); // 将 UTF-8 字符串解码回正常字符串
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* 将base64编码的字符串转换为原始字符串,包括对中文内容的处理(高性能,且支持Web、Node、小程序等任意平台)
|
|
2909
|
+
* @param base64 base64编码的字符串
|
|
2910
|
+
* @returns 原始字符串,包括中文内容
|
|
2911
|
+
*/
|
|
2912
|
+
function b64decode(base64) {
|
|
2913
|
+
const binaryString = !isNullOrUnDef(getGlobal('atob')) ? getGlobal('atob')(base64) : weAtob(base64);
|
|
2914
|
+
const len = binaryString.length;
|
|
2915
|
+
const bytes = new Uint8Array(len);
|
|
2916
|
+
for (let i = 0; i < len; i++) {
|
|
2917
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
2918
|
+
}
|
|
2919
|
+
// 使用TextDecoder将Uint8Array转换为原始字符串,包括中文内容
|
|
2920
|
+
return !isNullOrUnDef(getGlobal('TextDecoder'))
|
|
2921
|
+
? new (getGlobal('TextDecoder'))('utf-8').decode(bytes)
|
|
2922
|
+
: uint8ArrayToString(bytes);
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* 将原始字符串,包括中文内容,转换为base64编码的字符串(高性能,且支持Web、Node、小程序等任意平台)
|
|
2926
|
+
* @param rawStr 原始字符串,包括中文内容
|
|
2927
|
+
* @returns base64编码的字符串
|
|
2928
|
+
*/
|
|
2929
|
+
function b64encode(rawStr) {
|
|
2930
|
+
const utf8Array = !isNullOrUnDef(getGlobal('TextEncoder'))
|
|
2931
|
+
? new (getGlobal('TextEncoder'))().encode(rawStr)
|
|
2932
|
+
: stringToUint8Array(rawStr);
|
|
2933
|
+
// 将 Uint8Array 转换为二进制字符串
|
|
2934
|
+
let binaryString = '';
|
|
2935
|
+
const len = utf8Array.length;
|
|
2936
|
+
for (let i = 0; i < len; i++) {
|
|
2937
|
+
binaryString += String.fromCharCode(utf8Array[i]);
|
|
2938
|
+
}
|
|
2939
|
+
// 将二进制字符串转换为base64编码的字符串
|
|
2940
|
+
return !isNullOrUnDef(getGlobal('btoa')) ? getGlobal('btoa')(binaryString) : weBtoa(binaryString);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// 邮箱
|
|
2944
|
+
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
2945
|
+
/**
|
|
2946
|
+
* 判断字符串是否为邮箱格式,不对邮箱真实性做验证,如域名是否正确等
|
|
2947
|
+
* @param {string} value
|
|
2948
|
+
* @returns {boolean}
|
|
2949
|
+
*/
|
|
2950
|
+
const isEmail = (value) => EMAIL_REGEX.test(value);
|
|
2951
|
+
// 手机号码 (中国大陆)
|
|
2952
|
+
// reference: https://www.runoob.com/regexp/regexp-syntax.html (?: 是非捕获元之一)
|
|
2953
|
+
const PHONE_REGEX = /^(?:(?:\+|00)86)?1\d{10}$/;
|
|
2954
|
+
/**
|
|
2955
|
+
* 判断字符串是否为宽松手机格式,即首位为 1 的 11 位数字都属于手机号
|
|
2956
|
+
* @param {string} value
|
|
2957
|
+
* @returns {boolean}
|
|
2958
|
+
*/
|
|
2959
|
+
const isPhone = (value) => PHONE_REGEX.test(value);
|
|
2960
|
+
// 身份证号码
|
|
2961
|
+
// http://www.stats.gov.cn/tjsj/tjbz/xzqhdm/
|
|
2962
|
+
// ["北京市", "天津市", "河北省", "山西省", "内蒙古自治区",
|
|
2963
|
+
// "辽宁省", "吉林省", "黑龙江省",
|
|
2964
|
+
// "上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省",
|
|
2965
|
+
// "河南省", "湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省",
|
|
2966
|
+
// "重庆市", "四川省", "贵州省", "云南省", "西藏自治区",
|
|
2967
|
+
// "陕西省", "甘肃省", "青海省","宁夏回族自治区", "新疆维吾尔自治区",
|
|
2968
|
+
// "台湾省",
|
|
2969
|
+
// "香港特别行政区", "澳门特别行政区"]
|
|
2970
|
+
// ["11", "12", "13", "14", "15",
|
|
2971
|
+
// "21", "22", "23",
|
|
2972
|
+
// "31", "32", "33", "34", "35", "36", "37",
|
|
2973
|
+
// "41", "42", "43", "44", "45", "46",
|
|
2974
|
+
// "50", "51", "52", "53", "54",
|
|
2975
|
+
// "61", "62", "63", "64", "65",
|
|
2976
|
+
// "71",
|
|
2977
|
+
// "81", "82"]
|
|
2978
|
+
// 91 国外
|
|
2979
|
+
const IDNO_RE = /^(1[1-5]|2[1-3]|3[1-7]|4[1-6]|5[0-4]|6[1-5]|7[1]|8[1-2]|9[1])\d{4}(18|19|20)\d{2}[01]\d[0123]\d{4}[\dxX]$/;
|
|
2980
|
+
/**
|
|
2981
|
+
* 判断字符串是否为身份证号码格式
|
|
2982
|
+
* @param {string} value
|
|
2983
|
+
* @returns {boolean}
|
|
2984
|
+
*/
|
|
2985
|
+
const isIdNo = (value) => {
|
|
2986
|
+
const isSameFormat = IDNO_RE.test(value);
|
|
2987
|
+
if (!isSameFormat)
|
|
2988
|
+
return false;
|
|
2989
|
+
const year = Number(value.slice(6, 10));
|
|
2990
|
+
const month = Number(value.slice(10, 12));
|
|
2991
|
+
const date = Number(value.slice(12, 14));
|
|
2992
|
+
const d = new Date(year, month - 1, date);
|
|
2993
|
+
const isSameDate = d.getFullYear() === year && d.getMonth() + 1 === month && d.getDate() === date;
|
|
2994
|
+
if (!isSameDate)
|
|
2995
|
+
return false;
|
|
2996
|
+
// 将身份证号码前面的17位数分别乘以不同的系数;
|
|
2997
|
+
// 从第一位到第十七位的系数分别为:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2
|
|
2998
|
+
// 将这17位数字和系数相乘的结果相加;
|
|
2999
|
+
// 用加出来和除以11,看余数是多少;
|
|
3000
|
+
// 余数只可能有0-1-2-3-4-5-6-7-8-9-10这11个数字;
|
|
3001
|
+
// 其分别对应的最后一位身份证的号码为1-0-X-9-8-7-6-5-4-3-2
|
|
3002
|
+
// 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2。
|
|
3003
|
+
const coefficientList = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
|
3004
|
+
const residueList = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
|
3005
|
+
let sum = 0;
|
|
3006
|
+
for (let start = 0; start < 17; start++) {
|
|
3007
|
+
sum += Number(value.slice(start, start + 1)) * coefficientList[start];
|
|
3008
|
+
}
|
|
3009
|
+
return residueList[sum % 11] === value.slice(-1);
|
|
3010
|
+
};
|
|
3011
|
+
const URL_REGEX = /^(https?|ftp):\/\/([^\s/$.?#].[^\s]*)$/i;
|
|
3012
|
+
const HTTP_URL_REGEX = /^https?:\/\/([^\s/$.?#].[^\s]*)$/i;
|
|
3013
|
+
/**
|
|
3014
|
+
* 判断字符串是否为 url 格式,支持 http、https、ftp 协议,支持域名或者 ipV4
|
|
3015
|
+
* @param {string} value
|
|
3016
|
+
* @returns {boolean}
|
|
3017
|
+
*/
|
|
3018
|
+
const isUrl = (url, includeFtp = false) => {
|
|
3019
|
+
const regex = includeFtp ? URL_REGEX : HTTP_URL_REGEX;
|
|
3020
|
+
return regex.test(url);
|
|
3021
|
+
};
|
|
3022
|
+
// ipv4
|
|
3023
|
+
const IPV4_REGEX = /^(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/;
|
|
3024
|
+
// ipv6
|
|
3025
|
+
const IPV6_REGEX = /^(([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|([\da-fA-F]{1,4}:){1,7}:|([\da-fA-F]{1,4}:){1,6}:[\da-fA-F]{1,4}|([\da-fA-F]{1,4}:){1,5}(:[\da-fA-F]{1,4}){1,2}|([\da-fA-F]{1,4}:){1,4}(:[\da-fA-F]{1,4}){1,3}|([\da-fA-F]{1,4}:){1,3}(:[\da-fA-F]{1,4}){1,4}|([\da-fA-F]{1,4}:){1,2}(:[\da-fA-F]{1,4}){1,5}|[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,6})|:((:[\da-fA-F]{1,4}){1,7}|:)|fe80:(:[\da-fA-F]{0,4}){0,4}%[\da-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([\da-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))$/i;
|
|
3026
|
+
/**
|
|
3027
|
+
* 判断字符串是否为 IPV4 格式,不对 ip 真实性做验证
|
|
3028
|
+
* @param {string} value
|
|
3029
|
+
* @returns {boolean}
|
|
3030
|
+
*/
|
|
3031
|
+
const isIpV4 = (value) => IPV4_REGEX.test(value);
|
|
3032
|
+
/**
|
|
3033
|
+
* 判断字符串是否为 IPV6 格式,不对 ip 真实性做验证
|
|
3034
|
+
* @param {string} value
|
|
3035
|
+
* @returns {boolean}
|
|
3036
|
+
*/
|
|
3037
|
+
const isIpV6 = (value) => IPV6_REGEX.test(value);
|
|
3038
|
+
const INTEGER_RE = /^(-?[1-9]\d*|0)$/;
|
|
3039
|
+
/**
|
|
3040
|
+
* 判断字符串是否为整数(自然数),即 ...,-3,-2,-1,0,1,2,3,...
|
|
3041
|
+
* @param {string} value
|
|
3042
|
+
* @returns {boolean}
|
|
3043
|
+
*/
|
|
3044
|
+
const isInteger = (value) => INTEGER_RE.test(value);
|
|
3045
|
+
const FLOAT_RE = /^-?([1-9]\d*|0)\.\d*[1-9]$/;
|
|
3046
|
+
/**
|
|
3047
|
+
* 判断字符串是否为浮点数,即必须有小数点的有理数
|
|
3048
|
+
* @param {string} value
|
|
3049
|
+
* @returns {boolean}
|
|
3050
|
+
*/
|
|
3051
|
+
const isFloat = (value) => FLOAT_RE.test(value);
|
|
3052
|
+
/**
|
|
3053
|
+
* 判断字符串是否为正确数值,包括整数和浮点数
|
|
3054
|
+
* @param {string} value
|
|
3055
|
+
* @returns {boolean}
|
|
3056
|
+
*/
|
|
3057
|
+
const isNumerical = (value) => isInteger(value) || isFloat(value);
|
|
3058
|
+
const DIGIT_RE = /^\d+$/;
|
|
3059
|
+
/**
|
|
3060
|
+
* 判断字符串是否为数字,例如六位数字短信验证码(093031)
|
|
3061
|
+
* @param {string} value
|
|
3062
|
+
* @returns {boolean}
|
|
3063
|
+
*/
|
|
3064
|
+
const isDigit = (value) => DIGIT_RE.test(value);
|
|
3065
|
+
|
|
3066
|
+
/**
|
|
3067
|
+
* 去除字符串中重复字符
|
|
3068
|
+
* @param {string} str
|
|
3069
|
+
* @returns string
|
|
3070
|
+
* @example
|
|
3071
|
+
*
|
|
3072
|
+
* uniqueSymbol('1a1bac');
|
|
3073
|
+
* // => '1abc'
|
|
3074
|
+
*/
|
|
3075
|
+
function uniqueSymbol(str) {
|
|
3076
|
+
return [...new Set(str.trim().split(''))].join('');
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* 转义所有特殊字符
|
|
3080
|
+
* @param {string} str 原字符串
|
|
3081
|
+
* reference: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_expressions
|
|
3082
|
+
* @returns string
|
|
3083
|
+
*/
|
|
3084
|
+
function escapeRegExp(str) {
|
|
3085
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); //$&表示整个被匹配的字符串
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* 根据左右匹配符号生产解析变量(自动删除变量内的空白)
|
|
3089
|
+
* @param {string} leftMatchSymbol
|
|
3090
|
+
* @param {string} rightMatchSymbol
|
|
3091
|
+
* @returns RegExp
|
|
3092
|
+
*/
|
|
3093
|
+
function parseVariableRegExp(leftMatchSymbol, rightMatchSymbol) {
|
|
3094
|
+
return new RegExp(`${escapeRegExp(leftMatchSymbol.trim())}\\s*([^${escapeRegExp(uniqueSymbol(leftMatchSymbol))}${escapeRegExp(uniqueSymbol(rightMatchSymbol))}\\s]*)\\s*${rightMatchSymbol.trim()}`, 'g');
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* 解析字符串的插值变量
|
|
3098
|
+
* @param {string} str 字符串
|
|
3099
|
+
* @param {string} leftMatchSymbol 变量左侧匹配符号,默认:{
|
|
3100
|
+
* @param {string} rightMatchSymbol 变量右侧匹配符号,默认:}
|
|
3101
|
+
* @returns string[]
|
|
3102
|
+
* @example
|
|
3103
|
+
*
|
|
3104
|
+
* default match symbol {} same as /{\s*([^{}\s]*)\s*}/g
|
|
3105
|
+
*/
|
|
3106
|
+
function parseVarFromString(str, leftMatchSymbol = '{', rightMatchSymbol = '}') {
|
|
3107
|
+
// @ts-ignore
|
|
3108
|
+
return Array.from(str.matchAll(parseVariableRegExp(leftMatchSymbol, rightMatchSymbol))).map(el => isNullOrUnDef(el) ? void 0 : el[1]);
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* 替换字符串中的插值变量
|
|
3112
|
+
* @param {string} sourceStr
|
|
3113
|
+
* @param {Record<string, any>} targetObj
|
|
3114
|
+
* @param {string} leftMatchSymbol 变量左侧匹配符号,默认:{
|
|
3115
|
+
* @param {string} rightMatchSymbol 变量右侧匹配符号,默认:}
|
|
3116
|
+
* @returns string
|
|
3117
|
+
*/
|
|
3118
|
+
function replaceVarFromString(sourceStr, targetObj, leftMatchSymbol = '{', rightMatchSymbol = '}') {
|
|
3119
|
+
return sourceStr.replace(new RegExp(parseVariableRegExp(leftMatchSymbol, rightMatchSymbol)), function (m, p1) {
|
|
3120
|
+
return objectHas(targetObj, p1) ? targetObj[p1] : m;
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* 在指定作用域中执行代码
|
|
3125
|
+
* @param {string} code 要执行的代码(需包含 return 语句或表达式)
|
|
3126
|
+
* @param {Object} scope 作用域对象(键值对形式的变量环境)
|
|
3127
|
+
* @returns 代码执行结果
|
|
3128
|
+
*
|
|
3129
|
+
* @example
|
|
3130
|
+
* // 测试用例 1: 基本变量访问
|
|
3131
|
+
* executeInScope("return a + b;", { a: 1, b: 2 });
|
|
3132
|
+
* // 3
|
|
3133
|
+
*
|
|
3134
|
+
* // 测试用例 2: 支持复杂表达式和运算
|
|
3135
|
+
* executeInScope(
|
|
3136
|
+
* "return Array.from({ length: 3 }, (_, i) => base + i);",
|
|
3137
|
+
* { base: 100 }
|
|
3138
|
+
* );
|
|
3139
|
+
* // [100, 101, 102]
|
|
3140
|
+
*
|
|
3141
|
+
* // 支持外传函数作用域执行
|
|
3142
|
+
* const scope = {
|
|
3143
|
+
* $: {
|
|
3144
|
+
* fun: {
|
|
3145
|
+
* time: {
|
|
3146
|
+
* now: function () {
|
|
3147
|
+
* return new Date();
|
|
3148
|
+
* },
|
|
3149
|
+
* },
|
|
3150
|
+
* },
|
|
3151
|
+
* },
|
|
3152
|
+
* };
|
|
3153
|
+
* executeInScope("return $.fun.time.now()", scope)
|
|
3154
|
+
*/
|
|
3155
|
+
function executeInScope(code, scope = {}) {
|
|
3156
|
+
// 提取作用域对象的键和值
|
|
3157
|
+
const keys = Object.keys(scope);
|
|
3158
|
+
const values = keys.map(key => scope[key]);
|
|
3159
|
+
try {
|
|
3160
|
+
// 动态创建函数,将作用域的键作为参数,代码作为函数体
|
|
3161
|
+
const func = new Function(...keys, `return (() => { ${code} })()`);
|
|
3162
|
+
// 调用函数并传入作用域的值
|
|
3163
|
+
return func(...values);
|
|
3164
|
+
}
|
|
3165
|
+
catch (error) {
|
|
3166
|
+
throw new Error(`代码执行失败: ${error.message}`);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
/**
|
|
3171
|
+
* 深拷贝堪称完全体 即:任何类型的数据都会被深拷贝
|
|
3172
|
+
*
|
|
3173
|
+
* 包含对null、原始值、对象循环引用的处理
|
|
3174
|
+
*
|
|
3175
|
+
* 对Map、Set、ArrayBuffer、Date、RegExp、Array、Object及原型链属性方法执行深拷贝
|
|
3176
|
+
* @param {T} source
|
|
3177
|
+
* @param {WeakMap} map
|
|
3178
|
+
* @returns {T}
|
|
3179
|
+
*/
|
|
3180
|
+
function cloneDeep(source, map = new WeakMap()) {
|
|
3181
|
+
// 处理原始类型和 null/undefined
|
|
3182
|
+
if (source === null || typeof source !== 'object') {
|
|
3183
|
+
return source;
|
|
3184
|
+
}
|
|
3185
|
+
// 处理循环引用
|
|
3186
|
+
if (map.has(source)) {
|
|
3187
|
+
return map.get(source);
|
|
3188
|
+
}
|
|
3189
|
+
// 处理 ArrayBuffer
|
|
3190
|
+
if (source instanceof ArrayBuffer) {
|
|
3191
|
+
const copy = new ArrayBuffer(source.byteLength);
|
|
3192
|
+
new Uint8Array(copy).set(new Uint8Array(source));
|
|
3193
|
+
map.set(source, copy);
|
|
3194
|
+
return copy;
|
|
3195
|
+
}
|
|
3196
|
+
// 处理 DataView 和 TypedArray (Uint8Array 等)
|
|
3197
|
+
if (ArrayBuffer.isView(source)) {
|
|
3198
|
+
const constructor = source.constructor;
|
|
3199
|
+
const bufferCopy = cloneDeep(source.buffer, map);
|
|
3200
|
+
return new constructor(bufferCopy, source.byteOffset, source.length);
|
|
3201
|
+
}
|
|
3202
|
+
// 处理 Date 对象
|
|
3203
|
+
if (source instanceof Date) {
|
|
3204
|
+
const copy = new Date(source.getTime());
|
|
3205
|
+
map.set(source, copy);
|
|
3206
|
+
return copy;
|
|
3207
|
+
}
|
|
3208
|
+
// 处理 RegExp 对象
|
|
3209
|
+
if (source instanceof RegExp) {
|
|
3210
|
+
const copy = new RegExp(source.source, source.flags);
|
|
3211
|
+
copy.lastIndex = source.lastIndex; // 保留匹配状态
|
|
3212
|
+
map.set(source, copy);
|
|
3213
|
+
return copy;
|
|
3214
|
+
}
|
|
3215
|
+
// 处理 Map
|
|
3216
|
+
if (source instanceof Map) {
|
|
3217
|
+
const copy = new Map();
|
|
3218
|
+
map.set(source, copy);
|
|
3219
|
+
source.forEach((value, key) => {
|
|
3220
|
+
copy.set(cloneDeep(key, map), cloneDeep(value, map));
|
|
3221
|
+
});
|
|
3222
|
+
return copy;
|
|
3223
|
+
}
|
|
3224
|
+
// 处理 Set
|
|
3225
|
+
if (source instanceof Set) {
|
|
3226
|
+
const copy = new Set();
|
|
3227
|
+
map.set(source, copy);
|
|
3228
|
+
source.forEach(value => {
|
|
3229
|
+
copy.add(cloneDeep(value, map));
|
|
3230
|
+
});
|
|
3231
|
+
return copy;
|
|
3232
|
+
}
|
|
3233
|
+
// 处理数组 (包含稀疏数组)
|
|
3234
|
+
if (Array.isArray(source)) {
|
|
3235
|
+
const copy = new Array(source.length);
|
|
3236
|
+
map.set(source, copy);
|
|
3237
|
+
// 克隆所有有效索引
|
|
3238
|
+
for (let i = 0, len = source.length; i < len; i++) {
|
|
3239
|
+
if (i in source) {
|
|
3240
|
+
// 保留空位
|
|
3241
|
+
copy[i] = cloneDeep(source[i], map);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
// 克隆数组的自定义属性
|
|
3245
|
+
const descriptors = Object.getOwnPropertyDescriptors(source);
|
|
3246
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
3247
|
+
Object.defineProperty(copy, key, {
|
|
3248
|
+
...descriptors[key],
|
|
3249
|
+
value: cloneDeep(descriptors[key].value, map)
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
return copy;
|
|
3253
|
+
}
|
|
3254
|
+
// 处理普通对象和类实例
|
|
3255
|
+
const copy = Object.create(Object.getPrototypeOf(source));
|
|
3256
|
+
map.set(source, copy);
|
|
3257
|
+
const descriptors = Object.getOwnPropertyDescriptors(source);
|
|
3258
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
3259
|
+
const descriptor = descriptors[key];
|
|
3260
|
+
if ('value' in descriptor) {
|
|
3261
|
+
// 克隆数据属性
|
|
3262
|
+
descriptor.value = cloneDeep(descriptor.value, map);
|
|
3263
|
+
}
|
|
3264
|
+
else {
|
|
3265
|
+
// 处理访问器属性 (getter/setter)
|
|
3266
|
+
if (descriptor.get) {
|
|
3267
|
+
descriptor.get = cloneDeep(descriptor.get, map);
|
|
3268
|
+
}
|
|
3269
|
+
if (descriptor.set) {
|
|
3270
|
+
descriptor.set = cloneDeep(descriptor.set, map);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
Object.defineProperty(copy, key, descriptor);
|
|
3274
|
+
}
|
|
3275
|
+
return copy;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
export { EMAIL_REGEX, HEX_POOL, HTTP_URL_REGEX, IPV4_REGEX, IPV6_REGEX, PHONE_REGEX, STRING_ARABIC_NUMERALS, STRING_LOWERCASE_ALPHA, STRING_POOL, STRING_UPPERCASE_ALPHA, UNIQUE_NUMBER_SAFE_LENGTH, URL_REGEX, add, addClass, arrayEach, arrayEachAsync, arrayInsertBefore, arrayLike, arrayRemove, asyncMap, b64decode, b64encode, calculateDate, calculateDateTime, chooseLocalFile, cloneDeep, compressImg, cookieDel, cookieGet, cookieSet, copyText, crossOriginDownload, dateParse, dateToEnd, dateToStart, debounce, divide, downloadBlob, downloadData, downloadHref, downloadURL, easingFunctional, easingStringify, escapeRegExp, executeInScope, fallbackCopyText, flatTree, forEachDeep, formatDate, formatNumber as formatMoney, formatNumber, formatTree, fuzzySearchTree, genCanvasWM, getComputedCssVal, getEasing, getGlobal, getStrWidthPx, getStyle, hasClass, humanFileSize, isArray, isBigInt, isBoolean, isDate, isDigit, isEmail, isEmpty, isError, isFloat, isFunction, isIdNo, isInteger, isIpV4, isIpV6, isJsonString, isNaN, isNodeList, isNull, isNullOrUnDef, isNullOrUnDef as isNullish, isNumber, isNumerical, isObject, isPhone, isPlainObject, isPrimitive, isRegExp, isString, isSymbol, isUndefined, isUrl, isValidDate, mapDeep, multiply, numberAbbr, numberToHex, objectAssign, objectEach, objectEachAsync, objectFill, objectGet, objectHas, objectMap, objectAssign as objectMerge, objectOmit, objectPick, once, parseQueryParams, parseVarFromString, pathJoin, pathNormalize, qsParse, qsStringify, randomNumber, randomString, randomUuid, removeClass, replaceVarFromString, safeAwait, searchTreeById, select, setEasing, setGlobal, setStyle, smoothScroll, stringAssign, stringCamelCase, stringEscapeHtml, stringFill, stringFormat, stringKebabCase, strip, subtract, supportCanvas, throttle, tooltipEvent, typeIs, uniqueNumber, uniqueString, uniqueSymbol, urlDelParams, urlParse, urlSetParams, urlStringify, wait, weAtob, weBtoa };
|