@lowentry/utils 1.25.2 → 2.0.2

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/src/LeUtils.js DELETED
@@ -1,3597 +0,0 @@
1
- import CloneDeep from 'clone-deep';
2
- import {ISSET, IS_OBJECT, IS_ARRAY, STRING, INT_LAX, FLOAT_LAX, INT_LAX_ANY, FLOAT_LAX_ANY, ARRAY} from './LeTypes.js';
3
-
4
-
5
- /**
6
- * @param {LeUtils_TransactionalValue} transactionalValue
7
- */
8
- const checkTransactionalValue = (transactionalValue) =>
9
- {
10
- if(!LeUtils.isTransactionalValueValid(transactionalValue))
11
- {
12
- console.error('The given value is not a valid TransactionalValue:');
13
- console.error(transactionalValue);
14
- throw new Error('The given value is not a valid TransactionalValue');
15
- }
16
- };
17
-
18
- /**
19
- * @param {LeUtils_TransactionalValue} transactionalValue
20
- * @param {string} changeId
21
- * @returns {{index:number, value:*}|null}
22
- */
23
- const findTransactionalValueChange = (transactionalValue, changeId) =>
24
- {
25
- for(let i = 0; i < transactionalValue.changes.length; i++)
26
- {
27
- const change = transactionalValue.changes[i];
28
- if(change.id === changeId)
29
- {
30
- return {index:i, value:change.value};
31
- }
32
- }
33
- return null;
34
- };
35
-
36
-
37
- export const LeUtils = {
38
- /**
39
- * A deep equals implementation.
40
- *
41
- * @param {*} a The value to compare.
42
- * @param {*} b The other value to compare.
43
- * @returns {boolean} Returns true if the values are equivalent.
44
- */
45
- equals:
46
- (a, b) =>
47
- {
48
- if(a === b)
49
- {
50
- return true;
51
- }
52
-
53
- if(a && b && typeof a == 'object' && typeof b == 'object')
54
- {
55
- if(a.constructor !== b.constructor)
56
- {
57
- return false;
58
- }
59
-
60
- if(Array.isArray(a))
61
- {
62
- const length = a.length;
63
- if(length != b.length)
64
- {
65
- return false;
66
- }
67
- for(let i = length; i-- !== 0;)
68
- {
69
- if(!LeUtils.equals(a[i], b[i]))
70
- {
71
- return false;
72
- }
73
- }
74
- return true;
75
- }
76
- if((a instanceof Map) && (b instanceof Map))
77
- {
78
- if(a.size !== b.size)
79
- {
80
- return false;
81
- }
82
- for(let i of a.entries())
83
- {
84
- if(!b.has(i[0]))
85
- {
86
- return false;
87
- }
88
- }
89
- for(let i of a.entries())
90
- {
91
- if(!LeUtils.equals(i[1], b.get(i[0])))
92
- {
93
- return false;
94
- }
95
- }
96
- return true;
97
- }
98
- if((a instanceof Set) && (b instanceof Set))
99
- {
100
- if(a.size !== b.size)
101
- {
102
- return false;
103
- }
104
- for(let i of a.entries())
105
- {
106
- if(!b.has(i[0]))
107
- {
108
- return false;
109
- }
110
- }
111
- return true;
112
- }
113
- if(ArrayBuffer.isView(a) && ArrayBuffer.isView(b))
114
- {
115
- if(('length' in a) && ('length' in b) && (typeof a.length === 'number') && (typeof b.length === 'number'))
116
- {
117
- if(a.length != b.length)
118
- {
119
- return false;
120
- }
121
- for(let i = a.length; i-- !== 0;)
122
- {
123
- if(a[i] !== b[i])
124
- {
125
- return false;
126
- }
127
- }
128
- return true;
129
- }
130
- if(('byteLength' in a) && ('byteLength' in b) && (typeof a.byteLength === 'number') && (typeof b.byteLength === 'number') && ('getUint8' in a) && ('getUint8' in b) && (typeof a.getUint8 === 'function') && (typeof b.getUint8 === 'function'))
131
- {
132
- if(a.byteLength !== b.byteLength)
133
- {
134
- return false;
135
- }
136
- for(let i = a.byteLength; i-- !== 0;)
137
- {
138
- if(a.getUint8(i) !== b.getUint8(i))
139
- {
140
- return false;
141
- }
142
- }
143
- return true;
144
- }
145
- return false;
146
- }
147
-
148
- if(a.constructor === RegExp)
149
- {
150
- return a.source === b.source && a.flags === b.flags;
151
- }
152
- if(a.valueOf !== Object.prototype.valueOf)
153
- {
154
- return a.valueOf() === b.valueOf();
155
- }
156
- if(a.toString !== Object.prototype.toString)
157
- {
158
- return a.toString() === b.toString();
159
- }
160
-
161
- if(a.constructor && (a.constructor !== Object) && (a.constructor !== Array) && (Object.getPrototypeOf(a) !== Object.prototype))
162
- {
163
- if(typeof a.equals === 'function')
164
- {
165
- return a.equals(b);
166
- }
167
- return false;
168
- }
169
-
170
- const keys = Object.keys(a);
171
- const length = keys.length;
172
- if(length !== Object.keys(b).length)
173
- {
174
- return false;
175
- }
176
- for(let i = length; i-- !== 0;)
177
- {
178
- if(!Object.prototype.hasOwnProperty.call(b, keys[i]))
179
- {
180
- return false;
181
- }
182
- }
183
- for(let i = length; i-- !== 0;)
184
- {
185
- const key = keys[i];
186
- if((key === '_owner') && a.$$typeof)
187
- {
188
- // React-specific: avoid traversing _owner, it contains circular references, and is not needed when comparing the actual element
189
- continue;
190
- }
191
- if(!LeUtils.equals(a[key], b[key]))
192
- {
193
- return false;
194
- }
195
- }
196
- return true;
197
- }
198
-
199
- // true if both are NaN, false otherwise
200
- return ((a !== a) && (b !== b));
201
- },
202
-
203
- /**
204
- * Performs a deep equality comparison between two collections (objects, maps, arrays, etc), sorting on the keys before comparing them.
205
- *
206
- * This is useful for comparing objects that have the same properties, but in a different order, and/or in a different collection type (like Maps vs Objects).
207
- *
208
- * @param {*} elementsA The elements to compare.
209
- * @param {*} elementsB The other elements to compare.
210
- * @param {string[]} [ignoreKeys=[]] An array of keys to ignore when comparing the elements. This is useful for ignoring properties that are not relevant for the comparison.
211
- * @return {boolean} Returns true if the given values are equivalent, ignoring the order of properties.
212
- */
213
- equalsMapLike:
214
- (() =>
215
- {
216
- const sortKeyValueArrays = (pairA, pairB) => LeUtils.compare(pairA[0], pairB[0]);
217
- return (elementsA, elementsB, ignoreKeys = []) =>
218
- {
219
- elementsA = LeUtils.mapToArray(elementsA, (value, key) => [key, value]).sort(sortKeyValueArrays);
220
- elementsB = LeUtils.mapToArray(elementsB, (value, key) => [key, value]).sort(sortKeyValueArrays);
221
- ignoreKeys = (typeof ignoreKeys === 'string') ? ARRAY(ignoreKeys) : LeUtils.mapToArray(ignoreKeys);
222
-
223
- let indexA = 0;
224
- let indexB = 0;
225
- while((indexA < elementsA.length) && (indexB < elementsB.length))
226
- {
227
- const [mapKey, mapValue] = elementsA[indexA];
228
- const [ownMapKey, ownMapValue] = elementsB[indexB];
229
-
230
- const ignoreKeysIncludesMapKey = ignoreKeys.includes(mapKey);
231
- const ignoreKeysIncludesOwnMapKey = ignoreKeys.includes(ownMapKey);
232
- if(ignoreKeysIncludesMapKey)
233
- {
234
- indexA++;
235
- if(ignoreKeysIncludesOwnMapKey)
236
- {
237
- indexB++;
238
- }
239
- continue;
240
- }
241
- else if(ignoreKeysIncludesOwnMapKey)
242
- {
243
- indexB++;
244
- continue;
245
- }
246
-
247
- if(!LeUtils.equals(mapKey, ownMapKey) || !LeUtils.equals(mapValue, ownMapValue))
248
- {
249
- return false;
250
- }
251
- indexA++;
252
- indexB++;
253
- }
254
-
255
- while((indexA < elementsA.length) && ignoreKeys.includes(elementsA[indexA][0]))
256
- {
257
- indexA++;
258
- }
259
- if(indexA < elementsA.length)
260
- {
261
- return false;
262
- }
263
-
264
- while((indexB < elementsB.length) && ignoreKeys.includes(elementsB[indexB][0]))
265
- {
266
- indexB++;
267
- }
268
- return (indexB >= elementsB.length);
269
- };
270
- })(),
271
-
272
- /**
273
- * Returns a deep copy of the given value.
274
- *
275
- * @param {*} value
276
- * @returns {*}
277
- */
278
- clone:
279
- (value) => CloneDeep(value, true),
280
-
281
- /**
282
- * Executes the given callback when the document is ready.
283
- *
284
- * @param {Function} callback
285
- * @returns {{remove:Function}}
286
- */
287
- onDomReady:
288
- (callback) =>
289
- {
290
- if(!globalThis?.document || !globalThis?.document?.addEventListener || !globalThis?.document?.removeEventListener)
291
- {
292
- // no document, so we can't wait for it to be ready
293
- console.warn('LeUtils.onDomReady() was called, but there is no document available. This might happen if the code is executed in a non-browser environment.');
294
- return {
295
- remove:() =>
296
- {
297
- },
298
- };
299
- }
300
-
301
- if((globalThis.document.readyState === 'interactive') || (globalThis.document.readyState === 'complete'))
302
- {
303
- return LeUtils.setTimeout(() => callback(), 0);
304
- }
305
- else
306
- {
307
- let listening = true;
308
- const callbackWrapper = () =>
309
- {
310
- if(listening)
311
- {
312
- listening = false;
313
- globalThis.document.removeEventListener('DOMContentLoaded', callbackWrapper);
314
- callback();
315
- }
316
- };
317
-
318
- globalThis.document.addEventListener('DOMContentLoaded', callbackWrapper);
319
-
320
- return {
321
- remove:
322
- () =>
323
- {
324
- if(listening)
325
- {
326
- listening = false;
327
- globalThis.document.removeEventListener('DOMContentLoaded', callbackWrapper);
328
- }
329
- },
330
- };
331
- }
332
- },
333
-
334
- /**
335
- * Parses the given version string, and returns an object with the major, minor, and patch numbers, as well as some comparison functions.
336
- *
337
- * Expects a version string such as:
338
- * - "1"
339
- * - "1.2"
340
- * - "1.2.3"
341
- * - "1.2.3 anything"
342
- * - "1.2.3-anything"
343
- *
344
- * @param {string|*} versionString
345
- * @returns {{major: (number), minor: (number), patch: (number), toString: (function(): string), equals: (function(string|*): boolean), smallerThan: (function(string|*): boolean), smallerThanOrEquals: (function(string|*): boolean), largerThan: (function(string|*): boolean), largerThanOrEquals: (function(string|*): boolean)}}
346
- */
347
- parseVersionString:
348
- (versionString) =>
349
- {
350
- if(IS_OBJECT(versionString) && ISSET(versionString?.major) && ISSET(versionString?.minor) && ISSET(versionString?.patch))
351
- {
352
- return versionString;
353
- }
354
-
355
- versionString = STRING(versionString).trim();
356
- const partsVersion = versionString.split(' ')[0].split('-')[0].split('.');
357
- const major = INT_LAX(partsVersion[0]);
358
- const minor = INT_LAX(partsVersion[1]);
359
- const patch = INT_LAX(partsVersion[2]);
360
-
361
- const THIS = {
362
- major:major,
363
- minor:minor,
364
- patch:patch,
365
-
366
- toString:
367
- () => major + '.' + minor + '.' + patch,
368
-
369
- equals:
370
- (otherVersion) =>
371
- {
372
- otherVersion = LeUtils.parseVersionString(otherVersion);
373
- return (major === otherVersion.major) && (minor === otherVersion.minor) && (patch === otherVersion.patch);
374
- },
375
-
376
- largerThan:
377
- (otherVersion) =>
378
- {
379
- otherVersion = LeUtils.parseVersionString(otherVersion);
380
-
381
- if(major > otherVersion.major)
382
- {
383
- return true;
384
- }
385
- if(major < otherVersion.major)
386
- {
387
- return false;
388
- }
389
-
390
- if(minor > otherVersion.minor)
391
- {
392
- return true;
393
- }
394
- if(minor < otherVersion.minor)
395
- {
396
- return false;
397
- }
398
-
399
- return (patch > otherVersion.patch);
400
- },
401
-
402
- largerThanOrEquals:
403
- (otherVersion) =>
404
- {
405
- otherVersion = LeUtils.parseVersionString(otherVersion);
406
-
407
- if(major > otherVersion.major)
408
- {
409
- return true;
410
- }
411
- if(major < otherVersion.major)
412
- {
413
- return false;
414
- }
415
-
416
- if(minor > otherVersion.minor)
417
- {
418
- return true;
419
- }
420
- if(minor < otherVersion.minor)
421
- {
422
- return false;
423
- }
424
-
425
- return (patch >= otherVersion.patch);
426
- },
427
-
428
- smallerThan:
429
- (otherVersion) => !THIS.largerThanOrEquals(otherVersion),
430
-
431
- smallerThanOrEquals:
432
- (otherVersion) => !THIS.largerThan(otherVersion),
433
- };
434
- return THIS;
435
- },
436
-
437
- /**
438
- * Returns true if the array or object contains the given value.
439
- *
440
- * Values are compared by casting both of them to a string.
441
- *
442
- * @param {*[]|Object|Function} array
443
- * @param {*} value
444
- * @returns {boolean}
445
- */
446
- contains:
447
- (array, value) =>
448
- {
449
- if(!array)
450
- {
451
- return false;
452
- }
453
- let result = false;
454
- value = STRING(value);
455
- LeUtils.each(array, (val) =>
456
- {
457
- if(STRING(val) === value)
458
- {
459
- result = true;
460
- return false;
461
- }
462
- });
463
- return result;
464
- },
465
-
466
- /**
467
- * Returns true if the array or object contains the given value.
468
- *
469
- * Values are compared by casting both of them to a string, and then lowercasing them.
470
- *
471
- * @param {*[]|Object|Function} array
472
- * @param {*} value
473
- * @returns {boolean}
474
- */
475
- containsCaseInsensitive:
476
- (array, value) =>
477
- {
478
- if(!array)
479
- {
480
- return false;
481
- }
482
- let result = false;
483
- value = STRING(value).toLowerCase();
484
- LeUtils.each(array, (val) =>
485
- {
486
- if(STRING(val).toLowerCase() === value)
487
- {
488
- result = true;
489
- return false;
490
- }
491
- });
492
- return result;
493
- },
494
-
495
- /**
496
- * Returns true if the array or object contains all the given values.
497
- *
498
- * Values are compared by casting both of them to a string.
499
- *
500
- * @param {*[]|Object|Function} array
501
- * @param {*[]|Object|Function} values
502
- * @returns {boolean}
503
- */
504
- containsAll:
505
- (array, values) =>
506
- {
507
- if(!array)
508
- {
509
- return false;
510
- }
511
- let result = true;
512
- LeUtils.each(values, (value) =>
513
- {
514
- if(!LeUtils.contains(array, value))
515
- {
516
- result = false;
517
- return false;
518
- }
519
- });
520
- return result;
521
- },
522
-
523
- /**
524
- * Returns true if the array or object contains all the given values.
525
- *
526
- * Values are compared by casting both of them to a string, and then lowercasing them.
527
- *
528
- * @param {*[]|Object|Function} array
529
- * @param {*[]|Object|Function} values
530
- * @returns {boolean}
531
- */
532
- containsAllCaseInsensitive:
533
- (array, values) =>
534
- {
535
- if(!array)
536
- {
537
- return false;
538
- }
539
- let result = true;
540
- LeUtils.each(values, (value) =>
541
- {
542
- if(!LeUtils.containsCaseInsensitive(array, value))
543
- {
544
- result = false;
545
- return false;
546
- }
547
- });
548
- return result;
549
- },
550
-
551
- /**
552
- * Returns true if the array or object contains any of the given values.
553
- *
554
- * Values are compared by casting both of them to a string.
555
- *
556
- * @param {*[]|Object|Function} array
557
- * @param {*[]|Object|Function} values
558
- * @returns {boolean}
559
- */
560
- containsAny:
561
- (array, values) =>
562
- {
563
- if(!array)
564
- {
565
- return false;
566
- }
567
- let result = false;
568
- LeUtils.each(values, (value) =>
569
- {
570
- if(LeUtils.contains(array, value))
571
- {
572
- result = true;
573
- return false;
574
- }
575
- });
576
- return result;
577
- },
578
-
579
- /**
580
- * Returns true if the array or object contains any of the given values.
581
- *
582
- * Values are compared by casting both of them to a string, and then lowercasing them.
583
- *
584
- * @param {*[]|Object|Function} array
585
- * @param {*[]|Object|Function} values
586
- * @returns {boolean}
587
- */
588
- containsAnyCaseInsensitive:
589
- (array, values) =>
590
- {
591
- if(!array)
592
- {
593
- return false;
594
- }
595
- let result = false;
596
- LeUtils.each(values, (value) =>
597
- {
598
- if(LeUtils.containsCaseInsensitive(array, value))
599
- {
600
- result = true;
601
- return false;
602
- }
603
- });
604
- return result;
605
- },
606
-
607
- /**
608
- * Returns true if the array or object contains none of the given values.
609
- *
610
- * Values are compared by casting both of them to a string.
611
- *
612
- * @param {*[]|Object|Function} array
613
- * @param {*[]|Object|Function} values
614
- * @returns {boolean}
615
- */
616
- containsNone:
617
- (array, values) =>
618
- {
619
- if(!array)
620
- {
621
- return true;
622
- }
623
- let result = true;
624
- LeUtils.each(values, (value) =>
625
- {
626
- if(LeUtils.contains(array, value))
627
- {
628
- result = false;
629
- return false;
630
- }
631
- });
632
- return result;
633
- },
634
-
635
- /**
636
- * Returns true if the array or object contains none of the given values.
637
- *
638
- * Values are compared by casting both of them to a string, and then lowercasing them.
639
- *
640
- * @param {*[]|Object|Function} array
641
- * @param {*[]|Object|Function} values
642
- * @returns {boolean}
643
- */
644
- containsNoneCaseInsensitive:
645
- (array, values) =>
646
- {
647
- if(!array)
648
- {
649
- return true;
650
- }
651
- let result = true;
652
- LeUtils.each(values, (value) =>
653
- {
654
- if(LeUtils.containsCaseInsensitive(array, value))
655
- {
656
- result = false;
657
- return false;
658
- }
659
- });
660
- return result;
661
- },
662
-
663
- /**
664
- * Finds the first element in the given array or object that returns true from the callback, and returns an object with the index and value.
665
- *
666
- * @param {*[]|Object|Function} elements
667
- * @param {(value:*, index:*) => boolean|void} callback
668
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
669
- * @returns {{index:*, value:*}|null}
670
- */
671
- findIndexValue:
672
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
673
- {
674
- let result = null;
675
- LeUtils.each(elements, (value, index) =>
676
- {
677
- if(callback.call(elements[index], elements[index], index))
678
- {
679
- result = {index, value};
680
- return false;
681
- }
682
- }, optionalSkipHasOwnPropertyCheck);
683
- return result;
684
- },
685
-
686
- /**
687
- * Finds the first element in the given array or object that returns true from the callback, and returns the index.
688
- *
689
- * @param {*[]|Object|Function} elements
690
- * @param {(value:*, index:*) => boolean|void} callback
691
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
692
- * @returns {*|null}
693
- */
694
- findIndex:
695
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) => LeUtils.findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.index ?? null,
696
-
697
- /**
698
- * Finds the first element in the given array or object that returns true from the callback, and returns the value.
699
- *
700
- * @param {*[]|Object|Function} elements
701
- * @param {(value:*, index:*) => boolean|void} callback
702
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
703
- * @returns {*|null}
704
- */
705
- find:
706
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) => LeUtils.findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.value ?? null,
707
-
708
- /**
709
- * Returns the value at the given index in the given elements.
710
- *
711
- * @param {*} elements
712
- * @param {*} index
713
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
714
- * @returns {*}
715
- */
716
- getValueAtIndex:
717
- (elements, index, optionalSkipHasOwnPropertyCheck = false) =>
718
- {
719
- if((elements === null) || (typeof elements === 'undefined'))
720
- {
721
- return undefined;
722
- }
723
- if(Array.isArray(elements))
724
- {
725
- return elements[index];
726
- }
727
- if((typeof elements === 'object') && (elements?.constructor === Object))
728
- {
729
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index))
730
- {
731
- return elements[index];
732
- }
733
- return undefined;
734
- }
735
- if(elements instanceof Map)
736
- {
737
- return elements.get(index);
738
- }
739
- if(elements instanceof Set)
740
- {
741
- return index;
742
- }
743
- if(typeof elements !== 'string')
744
- {
745
- if(ArrayBuffer.isView(elements) && !(elements instanceof DataView))
746
- {
747
- return elements[index];
748
- }
749
- if(typeof elements?.[Symbol.iterator] === 'function')
750
- {
751
- let i = 0;
752
- for(const value of elements)
753
- {
754
- if(i === index)
755
- {
756
- return value;
757
- }
758
- i++;
759
- }
760
- return undefined;
761
- }
762
- if(typeof elements?.forEach === 'function')
763
- {
764
- let result = undefined;
765
- let shouldContinue = true;
766
- elements.forEach((value, i) =>
767
- {
768
- if(shouldContinue)
769
- {
770
- if(i === index)
771
- {
772
- result = value;
773
- shouldContinue = false;
774
- }
775
- }
776
- });
777
- return result;
778
- }
779
- }
780
- if((typeof elements === 'object') || (typeof elements === 'function'))
781
- {
782
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index))
783
- {
784
- return elements[index];
785
- }
786
- return undefined;
787
- }
788
- return undefined;
789
- },
790
-
791
- /**
792
- * Checks if the given elements can be iterated over using LeUtils.each().
793
- *
794
- * @param {*} elements
795
- * @returns {boolean}
796
- */
797
- supportsEach:
798
- (elements) =>
799
- {
800
- if((elements === null) || (typeof elements === 'undefined') || (typeof elements === 'string'))
801
- {
802
- return false;
803
- }
804
- return !!(
805
- (Array.isArray(elements))
806
- || ((typeof elements === 'object') && (elements?.constructor === Object))
807
- || (typeof elements?.[Symbol.iterator] === 'function')
808
- || (typeof elements?.forEach === 'function')
809
- || ((typeof elements === 'object') || (typeof elements === 'function'))
810
- );
811
- },
812
-
813
- /**
814
- * Returns an iterator that iterates over each element in the given array or object, yielding an array with the value and the index/key.
815
- *
816
- * @param {*} elements
817
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
818
- * @yields {[key:*, value:*]}
819
- */
820
- eachIterator:
821
- function* (elements, optionalSkipHasOwnPropertyCheck = false)
822
- {
823
- if((elements === null) || (typeof elements === 'undefined'))
824
- {
825
- return;
826
- }
827
- if(Array.isArray(elements))
828
- {
829
- for(let i = 0; i < elements.length; i++)
830
- {
831
- yield [elements[i], i];
832
- }
833
- return;
834
- }
835
- if((typeof elements === 'object') && (elements?.constructor === Object))
836
- {
837
- for(const i in elements)
838
- {
839
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, i))
840
- {
841
- yield [elements[i], i];
842
- }
843
- }
844
- return;
845
- }
846
- if(elements instanceof Map)
847
- {
848
- for(const [i, value] of elements)
849
- {
850
- yield [value, i];
851
- }
852
- return;
853
- }
854
- if(elements instanceof Set)
855
- {
856
- for(const value of elements)
857
- {
858
- yield [value, value];
859
- }
860
- return;
861
- }
862
- if(typeof elements !== 'string')
863
- {
864
- if(typeof elements?.[Symbol.iterator] === 'function')
865
- {
866
- let i = 0;
867
- for(const value of elements)
868
- {
869
- yield [value, i];
870
- i++;
871
- }
872
- return;
873
- }
874
- if(typeof elements?.forEach === 'function')
875
- {
876
- const buffer = [];
877
- elements.forEach((value, i) =>
878
- {
879
- buffer.push([value, i]);
880
- });
881
- for(const entry of buffer)
882
- {
883
- yield entry;
884
- }
885
- return;
886
- }
887
- }
888
- if((typeof elements === 'object') || (typeof elements === 'function'))
889
- {
890
- for(const i in elements)
891
- {
892
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, i))
893
- {
894
- yield [elements[i], i];
895
- }
896
- }
897
- return;
898
- }
899
- console.warn('Executed LeUtils.eachIterator() on an invalid type: [' + (typeof elements) + ']', elements);
900
- },
901
-
902
- /**
903
- * Loops through each element in the given array or object, and calls the callback for each element.
904
- *
905
- * @param {*} elements
906
- * @param {(value:*, index?:*) => boolean|void} callback
907
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
908
- * @returns {*}
909
- */
910
- each:
911
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
912
- {
913
- for(const [value, key] of LeUtils.eachIterator(elements, optionalSkipHasOwnPropertyCheck))
914
- {
915
- if(callback.call(value, value, key) === false)
916
- {
917
- break;
918
- }
919
- }
920
- return elements;
921
- },
922
-
923
- /**
924
- * Like LeUtils.each(), except that it expects an async callback.
925
- *
926
- * @param {*} elements
927
- * @param {(value:*, index?:*) => Promise<boolean|undefined>} asyncCallback
928
- * @param {number} [optionalParallelCount]
929
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
930
- * @returns {Promise<*>}
931
- */
932
- eachAsync:
933
- (() =>
934
- {
935
- /**
936
- * Instead of waiting for every promise individually, this function will queue up multiple promises at once, then wait for any of them to finish, before adding more, until it has looped through all elements.
937
- * Then, at the end, it will wait for the remaining promises to finish.
938
- */
939
- const eachAsyncParallel = async (elements, asyncCallback, optionalParallelCount, optionalSkipHasOwnPropertyCheck) =>
940
- {
941
- const runningPromises = new Set();
942
- let doBreak = false;
943
- await LeUtils.eachAsync(elements, async (value, index) =>// loop through each element
944
- {
945
- if(doBreak)
946
- {
947
- return false;
948
- }
949
-
950
- // if no spot is available, wait for one to finish
951
- while(runningPromises.size >= optionalParallelCount)
952
- {
953
- await Promise.race(runningPromises);
954
- if(doBreak)
955
- {
956
- return false;
957
- }
958
- }
959
-
960
- // process this element, by creating a promise, and adding it to the queue
961
- const promise = (async () =>
962
- {
963
- if((await asyncCallback.call(value, value, index)) === false)
964
- {
965
- doBreak = true;
966
- }
967
- })();
968
- runningPromises.add(promise);
969
- promise.finally(() =>
970
- {
971
- runningPromises.delete(promise);
972
- });
973
- }, 1, optionalSkipHasOwnPropertyCheck);
974
- await Promise.all(runningPromises);
975
- return elements;
976
- };
977
-
978
- return async (elements, asyncCallback, parallelCount = 1, optionalSkipHasOwnPropertyCheck = false) =>
979
- {
980
- if((elements !== null) && (typeof elements !== 'undefined'))
981
- {
982
- parallelCount = INT_LAX(parallelCount);
983
- if(parallelCount > 1)
984
- {
985
- return await eachAsyncParallel(elements, asyncCallback, parallelCount, optionalSkipHasOwnPropertyCheck);
986
- }
987
-
988
- for(const [value, key] of LeUtils.eachIterator(elements, optionalSkipHasOwnPropertyCheck))
989
- {
990
- if((await asyncCallback.call(value, value, key)) === false)
991
- {
992
- break;
993
- }
994
- }
995
- }
996
- return elements;
997
- };
998
- })(),
999
-
1000
- /**
1001
- * Returns an empty simplified collection (array, object, or Map), based on the given elements.
1002
- *
1003
- * Usage:
1004
- *
1005
- * ```js
1006
- * const [success, collection, add] = LeUtils.getEmptySimplifiedCollection(elements);
1007
- * ```
1008
- *
1009
- * @param {*} elements
1010
- * @returns {[boolean, *[]|Object|Map, (value:*,index:*)=>void]}
1011
- */
1012
- getEmptySimplifiedCollection:
1013
- (elements) =>
1014
- {
1015
- if((elements === null) || (typeof elements === 'undefined'))
1016
- {
1017
- return [false, [], (value, index) =>
1018
- {
1019
- }];
1020
- }
1021
-
1022
- let collection = null;
1023
- let add = null;
1024
- if(Array.isArray(elements))
1025
- {
1026
- collection = [];
1027
- add = (value, index) =>
1028
- {
1029
- collection.push(value);
1030
- };
1031
- }
1032
- else if((typeof elements === 'object') && (elements?.constructor === Object))
1033
- {
1034
- collection = {};
1035
- add = (value, index) =>
1036
- {
1037
- collection[index] = value;
1038
- };
1039
- }
1040
- else if(elements instanceof Map)
1041
- {
1042
- collection = new Map();
1043
- add = (value, index) =>
1044
- {
1045
- collection.set(index, value);
1046
- };
1047
- }
1048
- else if((typeof elements !== 'string') && ((typeof elements?.[Symbol.iterator] === 'function') || (typeof elements?.forEach === 'function')))
1049
- {
1050
- collection = [];
1051
- add = (value, index) =>
1052
- {
1053
- collection.push(value);
1054
- };
1055
- }
1056
- else if((typeof elements === 'object') || (typeof elements === 'function'))
1057
- {
1058
- collection = {};
1059
- add = (value, index) =>
1060
- {
1061
- collection[index] = value;
1062
- };
1063
- }
1064
- else
1065
- {
1066
- console.warn('Executed LeUtils.getEmptySimplifiedCollection() on an invalid type: [' + (typeof elements) + ']', elements);
1067
- return [false, [], (value, index) =>
1068
- {
1069
- }];
1070
- }
1071
- return [true, collection, add];
1072
- },
1073
-
1074
- /**
1075
- * Loops through the given elements, and returns a new collection, with only the elements that returned true (or a value equals to true) from the callback.
1076
- * If no callback is given, it will return all elements that are of a true value (for example, values that are: not null, not undefined, not false, not 0, not an empty string, not an empty array, not an empty object).
1077
- *
1078
- * @param {*} elements
1079
- * @param {(value:*, index:*) => boolean|undefined} [callback]
1080
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1081
- * @returns {*}
1082
- */
1083
- filter:
1084
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
1085
- {
1086
- const [success, collection, add] = LeUtils.getEmptySimplifiedCollection(elements);
1087
- if(!success)
1088
- {
1089
- return elements;
1090
- }
1091
-
1092
- LeUtils.each(elements, (value, index) =>
1093
- {
1094
- if(!callback)
1095
- {
1096
- if(value)
1097
- {
1098
- add(value, index);
1099
- }
1100
- }
1101
- else if(callback.call(value, value, index))
1102
- {
1103
- add(value, index);
1104
- }
1105
- }, optionalSkipHasOwnPropertyCheck);
1106
- return collection;
1107
- },
1108
-
1109
- /**
1110
- * Loops through the given elements, and returns a new collection, with the elements that were returned from the callback.
1111
- *
1112
- * @param {*} elements
1113
- * @param {(value:*, index:*) => *} [callback]
1114
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1115
- * @returns {*}
1116
- */
1117
- map:
1118
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
1119
- {
1120
- const [success, collection, add] = LeUtils.getEmptySimplifiedCollection(elements);
1121
- if(!success)
1122
- {
1123
- return elements;
1124
- }
1125
-
1126
- LeUtils.each(elements, (value, index) =>
1127
- {
1128
- if(!callback)
1129
- {
1130
- add(value, index);
1131
- }
1132
- else
1133
- {
1134
- add(callback.call(value, value, index), index);
1135
- }
1136
- }, optionalSkipHasOwnPropertyCheck);
1137
- return collection;
1138
- },
1139
-
1140
- /**
1141
- * Loops through the given elements, and returns a new array, with the elements that were returned from the callback. Always returns an array.
1142
- *
1143
- * @param {*} elements
1144
- * @param {(value:*, index:*) => *} [callback]
1145
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1146
- * @returns {*[]}
1147
- */
1148
- mapToArray:
1149
- (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
1150
- {
1151
- let result = [];
1152
- LeUtils.each(elements, (value, index) =>
1153
- {
1154
- if(!callback)
1155
- {
1156
- result.push(value);
1157
- }
1158
- else
1159
- {
1160
- result.push(callback.call(value, value, index));
1161
- }
1162
- }, optionalSkipHasOwnPropertyCheck);
1163
- return result;
1164
- },
1165
-
1166
- /**
1167
- * Loops through the given elements, and returns a new array, with the elements that were returned from the callback. The elements will be sorted by the result from the given comparator. Always returns an array.
1168
- *
1169
- * @param {*} elements
1170
- * @param {(valueA:*, valueB:*) => number} comparator
1171
- * @param {(value:*, index:*) => *} [callback]
1172
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1173
- * @returns {*[]}
1174
- */
1175
- mapToArraySorted:
1176
- (elements, comparator, callback, optionalSkipHasOwnPropertyCheck = false) =>
1177
- {
1178
- const keys = LeUtils.sortKeys(elements, comparator, optionalSkipHasOwnPropertyCheck);
1179
- let result = [];
1180
- for(const key of keys)
1181
- {
1182
- const value = LeUtils.getValueAtIndex(elements, key, optionalSkipHasOwnPropertyCheck);
1183
- if(!callback)
1184
- {
1185
- result.push(value);
1186
- }
1187
- else
1188
- {
1189
- result.push(callback.call(value, value, key));
1190
- }
1191
- }
1192
- return result;
1193
- },
1194
-
1195
- /**
1196
- * Loops through the given elements, and returns a new array, with the keys from the given elements, sorted by the result from the given comparator. Always returns an array.
1197
- *
1198
- * @param {*} elements
1199
- * @param {(valueA:*, valueB:*) => number} comparator
1200
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1201
- * @returns {*[]}
1202
- */
1203
- sortKeys:
1204
- (elements, comparator, optionalSkipHasOwnPropertyCheck = false) =>
1205
- {
1206
- let keys = [];
1207
- LeUtils.each(elements, (value, index) =>
1208
- {
1209
- keys.push(index);
1210
- }, optionalSkipHasOwnPropertyCheck);
1211
-
1212
- keys.sort((a, b) => comparator(LeUtils.getValueAtIndex(elements, a, optionalSkipHasOwnPropertyCheck), LeUtils.getValueAtIndex(elements, b, optionalSkipHasOwnPropertyCheck)));
1213
- return keys;
1214
- },
1215
-
1216
- /**
1217
- * Turns the given value(s) into a 1 dimensional array.
1218
- *
1219
- * Does the same thing as Array.flat(Infinity).
1220
- *
1221
- * @param {*} array
1222
- * @returns {*[]}
1223
- */
1224
- flattenArray:
1225
- (() =>
1226
- {
1227
- const flattenArrayRecursive = (result, array) =>
1228
- {
1229
- if(!Array.isArray(array))
1230
- {
1231
- result.push(array);
1232
- return;
1233
- }
1234
- array.forEach((entry) =>
1235
- {
1236
- flattenArrayRecursive(result, entry);
1237
- });
1238
- };
1239
-
1240
- return (array) =>
1241
- {
1242
- if(!Array.isArray(array))
1243
- {
1244
- return [array];
1245
- }
1246
- let result = [];
1247
- array.forEach((entry) =>
1248
- {
1249
- flattenArrayRecursive(result, entry);
1250
- });
1251
- return result;
1252
- };
1253
- })(),
1254
-
1255
- /**
1256
- * Turns the given value(s) into a 1 dimensional array.
1257
- *
1258
- * Compared to LeUtils.flattenArray(), this function also supports objects, Maps, Sets, and other iterable objects.
1259
- *
1260
- * @param {*} elements
1261
- * @param {boolean} [optionalSkipHasOwnPropertyCheck]
1262
- * @returns {*[]}
1263
- */
1264
- flattenToArray:
1265
- (() =>
1266
- {
1267
- const flattenToArrayRecursive = (result, elements, optionalSkipHasOwnPropertyCheck) =>
1268
- {
1269
- if(!LeUtils.supportsEach(elements))
1270
- {
1271
- result.push(elements);
1272
- return;
1273
- }
1274
- LeUtils.each(elements, entry =>
1275
- {
1276
- flattenToArrayRecursive(result, entry, optionalSkipHasOwnPropertyCheck);
1277
- }, optionalSkipHasOwnPropertyCheck);
1278
- };
1279
-
1280
- return (elements, optionalSkipHasOwnPropertyCheck = false) =>
1281
- {
1282
- if(!LeUtils.supportsEach(elements))
1283
- {
1284
- return [elements];
1285
- }
1286
- let result = [];
1287
- LeUtils.each(elements, entry =>
1288
- {
1289
- flattenToArrayRecursive(result, entry, optionalSkipHasOwnPropertyCheck);
1290
- }, optionalSkipHasOwnPropertyCheck);
1291
- return result;
1292
- };
1293
- })(),
1294
-
1295
- /**
1296
- * Compares two values. Primarily used for sorting.
1297
- *
1298
- * @param {*} a
1299
- * @param {*} b
1300
- * @returns {number}
1301
- */
1302
- compare:
1303
- (a, b) => (a < b) ? -1 : ((a > b) ? 1 : 0),
1304
-
1305
- /**
1306
- * Compares two numbers. Primarily used for sorting.
1307
- *
1308
- * @param {number} a
1309
- * @param {number} b
1310
- * @returns {number}
1311
- */
1312
- compareNumbers:
1313
- (a, b) => a - b,
1314
-
1315
- /**
1316
- * Compares two numeric strings. Primarily used for sorting.
1317
- *
1318
- * @param {string|number} a
1319
- * @param {string|number} b
1320
- * @returns {number}
1321
- */
1322
- compareNumericStrings:
1323
- (a, b) =>
1324
- {
1325
- const aParts = STRING(a).split('.');
1326
- const bParts = STRING(b).split('.');
1327
- for(let i = 0; i < Math.min(aParts.length, bParts.length); i++)
1328
- {
1329
- a = aParts[i].trim();
1330
- b = bParts[i].trim();
1331
- if(a.length !== b.length)
1332
- {
1333
- return (a.length < b.length) ? -1 : 1;
1334
- }
1335
- if(a !== b)
1336
- {
1337
- return (a < b) ? -1 : 1;
1338
- }
1339
- }
1340
- if(aParts.length !== bParts.length)
1341
- {
1342
- return (aParts.length < bParts.length) ? -1 : 1;
1343
- }
1344
- return 0;
1345
- },
1346
-
1347
- /**
1348
- * Compares two strings in a natural way, meaning that it will compare numbers in the strings as actual numbers.
1349
- *
1350
- * This will correctly sort numeric parts so that "file5.txt" comes before "file10.txt", as well as that "file/5/test.txt" comes before "file/10/test.txt".
1351
- *
1352
- * @param {string} a
1353
- * @param {string} b
1354
- * @returns {number}
1355
- */
1356
- compareNaturalStrings:
1357
- (a, b) =>
1358
- {
1359
- const re = /(\d+|\D+)/g; // split into runs of digits or non-digits
1360
- const aTokens = a.match(re) ?? [];
1361
- const bTokens = b.match(re) ?? [];
1362
-
1363
- const len = Math.min(aTokens.length, bTokens.length);
1364
- for(let i = 0; i < len; i++)
1365
- {
1366
- const x = aTokens[i];
1367
- const y = bTokens[i];
1368
- if(x === y)
1369
- {
1370
- continue;
1371
- }
1372
-
1373
- // if both are numbers, compare as numbers
1374
- const nx = parseInt(x, 10);
1375
- const ny = parseInt(y, 10);
1376
- if(!isNaN(nx) && !isNaN(ny))
1377
- {
1378
- return nx - ny;
1379
- }
1380
-
1381
- // otherwise compare lexically
1382
- return x < y ? -1 : 1;
1383
- }
1384
-
1385
- return aTokens.length - bTokens.length;
1386
- },
1387
-
1388
- /**
1389
- * Compares two strings generated by LeUtils.timestamp(). Primarily used for sorting.
1390
- *
1391
- * @param {string} a
1392
- * @param {string} b
1393
- * @returns {number}
1394
- */
1395
- compareTimestampStrings:
1396
- (a, b) =>
1397
- {
1398
- a = LeUtils.base64ToHex(STRING(a).replaceAll('-', '+').replaceAll('_', '/'));
1399
- b = LeUtils.base64ToHex(STRING(b).replaceAll('-', '+').replaceAll('_', '/'));
1400
- return LeUtils.compare(a, b);
1401
- },
1402
-
1403
- /**
1404
- * Returns true if the given object is empty, false otherwise.
1405
- *
1406
- * @param {Object} obj
1407
- * @param [optionalSkipHasOwnPropertyCheck]
1408
- * @returns {boolean}
1409
- */
1410
- isEmptyObject:
1411
- (obj, optionalSkipHasOwnPropertyCheck = false) =>
1412
- {
1413
- for(let field in obj)
1414
- {
1415
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
1416
- {
1417
- return false;
1418
- }
1419
- }
1420
- return true;
1421
- },
1422
-
1423
- /**
1424
- * Returns the number of fields in the given object.
1425
- *
1426
- * @param {Object} obj
1427
- * @param [optionalSkipHasOwnPropertyCheck]
1428
- * @returns {number}
1429
- */
1430
- getObjectFieldsCount:
1431
- (obj, optionalSkipHasOwnPropertyCheck = false) =>
1432
- {
1433
- let count = 0;
1434
- for(let field in obj)
1435
- {
1436
- if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
1437
- {
1438
- count++;
1439
- }
1440
- }
1441
- return count;
1442
- },
1443
-
1444
- /**
1445
- * Returns true if the given function is a generator function (like: "function* (){}"), returns false otherwise.
1446
- *
1447
- * @param {Function} func
1448
- * @returns {boolean}
1449
- */
1450
- isGeneratorFunction:
1451
- (() =>
1452
- {
1453
- const GeneratorFunction = function* ()
1454
- {
1455
- }.constructor;
1456
-
1457
- const AsyncGeneratorFunction = async function* ()
1458
- {
1459
- }.constructor;
1460
-
1461
- const RegularFunction = function()
1462
- {
1463
- }.constructor;
1464
-
1465
- const PossibleGeneratorFunctionNames = Array.from(new Set(['GeneratorFunction', 'AsyncFunction', 'AsyncGeneratorFunction', GeneratorFunction.name, AsyncGeneratorFunction.name])).filter((element) =>
1466
- {
1467
- return (element && (element !== RegularFunction.name));
1468
- });
1469
-
1470
- return (func) =>
1471
- {
1472
- if(!func)
1473
- {
1474
- return false;
1475
- }
1476
- const constructor = func.constructor;
1477
- if(!constructor)
1478
- {
1479
- return false;
1480
- }
1481
- // noinspection JSUnresolvedVariable
1482
- return ((constructor.name && PossibleGeneratorFunctionNames.includes(constructor.name)) || (constructor.displayName && PossibleGeneratorFunctionNames.includes(constructor.displayName)));
1483
- };
1484
- })(),
1485
-
1486
- /**
1487
- * Executes the callback after the given number of milliseconds. Passes the elapsed time in seconds to the callback.
1488
- *
1489
- * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setTimeout((deltaTime)=>{}, 1000); timeoutHandler.remove();")
1490
- *
1491
- * @param {(deltaTime:number) => *} callback
1492
- * @param {number} ms
1493
- * @returns {{remove:Function}}
1494
- */
1495
- setTimeout:
1496
- (callback, ms) =>
1497
- {
1498
- if(!globalThis?.setTimeout || !globalThis?.clearTimeout)
1499
- {
1500
- console.warn('LeUtils.setTimeout() called in an environment without globalThis.setTimeout, returning a no-op handler.');
1501
- return {
1502
- remove:() =>
1503
- {
1504
- },
1505
- };
1506
- }
1507
-
1508
- ms = FLOAT_LAX(ms);
1509
-
1510
- let lastTime = globalThis?.performance?.now?.() ?? 0;
1511
- /** @type {number|null} */
1512
- let handler = globalThis.setTimeout(() =>
1513
- {
1514
- const currentTime = globalThis?.performance?.now?.() ?? 0;
1515
- try
1516
- {
1517
- callback((currentTime - lastTime) / 1000);
1518
- }
1519
- catch(e)
1520
- {
1521
- console.error(e);
1522
- }
1523
- lastTime = currentTime;
1524
- }, ms);
1525
-
1526
- return {
1527
- remove:
1528
- () =>
1529
- {
1530
- if(handler !== null)
1531
- {
1532
- globalThis.clearTimeout(handler);
1533
- handler = null;
1534
- }
1535
- },
1536
- };
1537
- },
1538
-
1539
- /**
1540
- * Executes the callback every given number of milliseconds. Passes the time difference in seconds between the last frame and now to it.
1541
- *
1542
- * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setInterval((deltaTime)=>{}, 1000); intervalHandler.remove();")
1543
- *
1544
- * @param {(deltaTime:number) => *} callback
1545
- * @param {number} [intervalMs]
1546
- * @param {boolean} [fireImmediately]
1547
- * @returns {{remove:Function}}
1548
- */
1549
- setInterval:
1550
- (callback, intervalMs = 1000, fireImmediately = false) =>
1551
- {
1552
- intervalMs = FLOAT_LAX_ANY(intervalMs, 1000);
1553
-
1554
- if(fireImmediately)
1555
- {
1556
- try
1557
- {
1558
- callback(0);
1559
- }
1560
- catch(e)
1561
- {
1562
- console.error(e);
1563
- }
1564
- }
1565
-
1566
- if(!globalThis?.setInterval || !globalThis?.clearInterval)
1567
- {
1568
- console.warn('LeUtils.setInterval() called in an environment without globalThis.setInterval, returning a no-op handler.');
1569
- return {
1570
- remove:() =>
1571
- {
1572
- },
1573
- };
1574
- }
1575
-
1576
- let lastTime = globalThis?.performance?.now?.() ?? 0;
1577
- /** @type {number|null} */
1578
- let handler = globalThis.setInterval(() =>
1579
- {
1580
- const currentTime = globalThis?.performance?.now?.() ?? 0;
1581
- try
1582
- {
1583
- callback((currentTime - lastTime) / 1000);
1584
- }
1585
- catch(e)
1586
- {
1587
- console.error(e);
1588
- }
1589
- lastTime = currentTime;
1590
- }, intervalMs);
1591
-
1592
- return {
1593
- remove:
1594
- () =>
1595
- {
1596
- if(handler !== null)
1597
- {
1598
- globalThis.clearInterval(handler);
1599
- handler = null;
1600
- }
1601
- },
1602
- };
1603
- },
1604
-
1605
- /**
1606
- * Executes the callback after the given number of frames. Passes the elapsed time in seconds to the callback.
1607
- *
1608
- * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setAnimationFrameTimeout((deltaTime){}, 5); timeoutHandler.remove();")
1609
- *
1610
- * @param {(deltaTime:number) => *} callback
1611
- * @param {number} [frames]
1612
- * @returns {{remove:Function}}
1613
- */
1614
- setAnimationFrameTimeout:
1615
- (callback, frames = 1) =>
1616
- {
1617
- if(!globalThis?.requestAnimationFrame || !globalThis?.cancelAnimationFrame)
1618
- {
1619
- console.warn('LeUtils.setAnimationFrameTimeout() called in an environment without globalThis.requestAnimationFrame, returning a no-op handler.');
1620
- return {
1621
- remove:() =>
1622
- {
1623
- },
1624
- };
1625
- }
1626
-
1627
- frames = INT_LAX_ANY(frames, 1);
1628
-
1629
- let run = true;
1630
- let requestAnimationFrameId = null;
1631
- let lastTime = globalThis?.performance?.now?.() ?? 0;
1632
- const tick = () =>
1633
- {
1634
- if(run)
1635
- {
1636
- if(frames <= 0)
1637
- {
1638
- run = false;
1639
- requestAnimationFrameId = null;
1640
- const currentTime = globalThis?.performance?.now?.() ?? 0;
1641
- try
1642
- {
1643
- callback((currentTime - lastTime) / 1000);
1644
- }
1645
- catch(e)
1646
- {
1647
- console.error(e);
1648
- }
1649
- lastTime = currentTime;
1650
- return;
1651
- }
1652
- frames--;
1653
- requestAnimationFrameId = globalThis.requestAnimationFrame(tick);
1654
- }
1655
- };
1656
- tick();
1657
-
1658
- return {
1659
- remove:
1660
- () =>
1661
- {
1662
- run = false;
1663
- if(requestAnimationFrameId !== null)
1664
- {
1665
- globalThis.cancelAnimationFrame(requestAnimationFrameId);
1666
- requestAnimationFrameId = null;
1667
- }
1668
- },
1669
- };
1670
- },
1671
-
1672
- /**
1673
- * Executes the callback every given number of frames. Passes the time difference in seconds between the last frame and now to it.
1674
- *
1675
- * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setAnimationFrameInterval((deltaTime)=>{}, 5); intervalHandler.remove();")
1676
- *
1677
- * @param {(deltaTime:number) => *} callback
1678
- * @param {number} [intervalFrames]
1679
- * @param {boolean} [fireImmediately]
1680
- * @returns {{remove:Function}}
1681
- */
1682
- setAnimationFrameInterval:
1683
- (callback, intervalFrames = 1, fireImmediately = false) =>
1684
- {
1685
- intervalFrames = INT_LAX_ANY(intervalFrames, 1);
1686
-
1687
- if(fireImmediately)
1688
- {
1689
- try
1690
- {
1691
- callback(0);
1692
- }
1693
- catch(e)
1694
- {
1695
- console.error(e);
1696
- }
1697
- }
1698
-
1699
- if(!globalThis?.requestAnimationFrame || !globalThis?.cancelAnimationFrame)
1700
- {
1701
- console.warn('LeUtils.setAnimationFrameInterval() called in an environment without globalThis.requestAnimationFrame, returning a no-op handler.');
1702
- return {
1703
- remove:() =>
1704
- {
1705
- },
1706
- };
1707
- }
1708
-
1709
- let run = true;
1710
- let requestAnimationFrameId = null;
1711
- let lastTimestamp = 0;
1712
- let totalTime = 0;
1713
- let frames = intervalFrames;
1714
- const tick = (timestamp) =>
1715
- {
1716
- if(run)
1717
- {
1718
- if(lastTimestamp === 0)
1719
- {
1720
- lastTimestamp = timestamp;
1721
- }
1722
- totalTime += (timestamp - lastTimestamp);
1723
- lastTimestamp = timestamp;
1724
-
1725
- frames--;
1726
- if(frames <= 0)
1727
- {
1728
- try
1729
- {
1730
- callback(totalTime / 1000);
1731
- }
1732
- catch(e)
1733
- {
1734
- console.error(e);
1735
- }
1736
- totalTime = 0;
1737
- frames = intervalFrames;
1738
- }
1739
-
1740
- if(run)
1741
- {
1742
- requestAnimationFrameId = globalThis.requestAnimationFrame(tick);
1743
- }
1744
- }
1745
- };
1746
- globalThis.requestAnimationFrame(tick);
1747
-
1748
- return {
1749
- remove:
1750
- () =>
1751
- {
1752
- run = false;
1753
- if(requestAnimationFrameId !== null)
1754
- {
1755
- globalThis.cancelAnimationFrame(requestAnimationFrameId);
1756
- requestAnimationFrameId = null;
1757
- }
1758
- },
1759
- };
1760
- },
1761
-
1762
- /**
1763
- * Returns a promise, which will be resolved after the given number of milliseconds.
1764
- *
1765
- * @param {number} ms
1766
- * @returns {Promise<number>}
1767
- */
1768
- promiseTimeout:
1769
- (ms) =>
1770
- {
1771
- ms = FLOAT_LAX(ms);
1772
- if(ms <= 0)
1773
- {
1774
- return new Promise(resolve => resolve(0));
1775
- }
1776
- return new Promise(resolve => LeUtils.setTimeout(resolve, ms));
1777
- },
1778
-
1779
- /**
1780
- * Returns a promise, which will be resolved after the given number of frames.
1781
- *
1782
- * @param {number} frames
1783
- * @returns {Promise<number>}
1784
- */
1785
- promiseAnimationFrameTimeout:
1786
- (frames) =>
1787
- {
1788
- frames = INT_LAX(frames);
1789
- if(frames <= 0)
1790
- {
1791
- return new Promise(resolve => resolve(0));
1792
- }
1793
- return new Promise(resolve => LeUtils.setAnimationFrameTimeout(resolve, frames));
1794
- },
1795
-
1796
- /**
1797
- * Allows you to do a fetch, with built-in retry and abort functionality.
1798
- *
1799
- * @param {string} url
1800
- * @param {{retries?:number|null, delay?:number|((attempt:number)=>number)|null}|Object|null} [options]
1801
- * @returns {{then:Function, catch:Function, finally:Function, remove:Function, isRemoved:Function}}
1802
- */
1803
- fetch:
1804
- (url, options) =>
1805
- {
1806
- let currentRetries = 0;
1807
- const retries = INT_LAX(options?.retries);
1808
-
1809
- let controllerAborted = false;
1810
- let controller = null;
1811
- if(globalThis?.AbortController)
1812
- {
1813
- controller = new AbortController();
1814
- }
1815
-
1816
- let promise = (async () =>
1817
- {
1818
- const attemptFetch = async () =>
1819
- {
1820
- if(controllerAborted || controller?.signal?.aborted)
1821
- {
1822
- throw new Error('Aborted');
1823
- }
1824
-
1825
- try
1826
- {
1827
- const response = await fetch(url, {
1828
- signal:controller?.signal,
1829
- ...(options ?? {}),
1830
- retries:undefined,
1831
- delay: undefined,
1832
- });
1833
- if(!response.ok)
1834
- {
1835
- throw new Error('Network request failed: ' + response.status + ' ' + response.statusText);
1836
- }
1837
- return response;
1838
- }
1839
- catch(error)
1840
- {
1841
- if(controllerAborted || controller?.signal?.aborted)
1842
- {
1843
- throw new Error('Aborted');
1844
- }
1845
- if(currentRetries >= retries)
1846
- {
1847
- throw error;
1848
- }
1849
- currentRetries++;
1850
- await LeUtils.promiseTimeout((typeof options?.delay === 'function') ? INT_LAX_ANY(options?.delay(currentRetries), 500) : (INT_LAX_ANY(options?.delay, 500)));
1851
- return await attemptFetch();
1852
- }
1853
- };
1854
- return await attemptFetch();
1855
- })();
1856
-
1857
- let result = {};
1858
- result.then = (...args) =>
1859
- {
1860
- promise = promise.then(...args);
1861
- return result;
1862
- };
1863
- result.catch = (...args) =>
1864
- {
1865
- promise = promise.catch(...args);
1866
- return result;
1867
- };
1868
- result.finally = (...args) =>
1869
- {
1870
- promise = promise.finally(...args);
1871
- return result;
1872
- };
1873
- result.remove = (...args) =>
1874
- {
1875
- controllerAborted = true;
1876
- if(controller)
1877
- {
1878
- controller.abort(...args);
1879
- }
1880
- return result;
1881
- };
1882
- result.isRemoved = () => (controllerAborted || !!controller?.signal?.aborted);
1883
- return result;
1884
- },
1885
-
1886
- /**
1887
- * Allows you to do a fetch, with built-in retry functionality. Caches on the requested URL, so that the same URL will not be fetched multiple times.
1888
- *
1889
- * @param {string} url
1890
- * @param {{retries?:number|null, delay?:number|((attempt:number)=>number)|null, [verify]:((data:*,response:*)=>void)|null}|null} [options]
1891
- * @param {((response:*)=>*)|null} [responseFunction] A function that will be called with the response object, and should return the data to be cached.
1892
- * @returns {Promise<*>}
1893
- */
1894
- cachedFetch:
1895
- (() =>
1896
- {
1897
- const cache = new Map();
1898
- return async (url, options, responseFunction) =>
1899
- {
1900
- if(cache.has(url))
1901
- {
1902
- const result = cache.get(url);
1903
- if(result.data)
1904
- {
1905
- return result.data;
1906
- }
1907
- if(result.promise)
1908
- {
1909
- return await result.promise;
1910
- }
1911
- if(result.error)
1912
- {
1913
- throw result.error;
1914
- }
1915
- console.warn('Failed to use the cachedFetch cache, for URL: ', url, ', it is in an unexpected state: ', result);
1916
- return null;
1917
- }
1918
-
1919
- const promise = LeUtils.fetch(url, options)
1920
- .then(async response =>
1921
- {
1922
- const data = responseFunction ? (await responseFunction(response)) : response;
1923
- if(typeof options?.verify === 'function')
1924
- {
1925
- await options.verify(data, response);
1926
- }
1927
- return data;
1928
- })
1929
- .then(data =>
1930
- {
1931
- cache.set(url, {data});
1932
- return data;
1933
- })
1934
- .catch(error =>
1935
- {
1936
- cache.set(url, {error});
1937
- console.error('Failed to fetch: ', error);
1938
- throw error;
1939
- });
1940
- if(!cache.has(url))
1941
- {
1942
- cache.set(url, {promise});
1943
- }
1944
- return await promise;
1945
- };
1946
- })(),
1947
-
1948
- /**
1949
- * Returns true if the user is on a smartphone device (mobile).
1950
- * Will return false if the user is on a tablet or on a desktop.
1951
- *
1952
- * In short:
1953
- * - Mobile: True
1954
- * - Tablet: False
1955
- * - Desktop: False
1956
- *
1957
- * @returns {boolean}
1958
- */
1959
- platformIsMobile:
1960
- () =>
1961
- {
1962
- // noinspection JSDeprecatedSymbols, JSUnresolvedReference
1963
- /** navigator.userAgentData.mobile doesn't return the correct value on some platforms, so this is a work-around, code from: http://detectmobilebrowsers.com **/
1964
- const a = STRING(globalThis?.navigator?.userAgent || globalThis?.navigator?.vendor || globalThis?.opera || '');
1965
- const b = a.substring(0, 4);
1966
- return !!(
1967
- /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
1968
- .test(a) ||
1969
- /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i
1970
- .test(b)
1971
- );
1972
- },
1973
-
1974
- /**
1975
- * Returns true if the user has a cursor (mouse, touchpad, etc).
1976
- * In this context, a cursor is defined as an input device that can hover over elements without necessarily interacting with them.
1977
- *
1978
- * @returns {boolean}
1979
- */
1980
- platformHasCursor:
1981
- () =>
1982
- {
1983
- return !LeUtils.platformIsMobile() && !globalThis?.matchMedia?.('(any-hover: none)')?.matches;
1984
- },
1985
-
1986
- /**
1987
- * Returns the given string, with the first character capitalized.
1988
- *
1989
- * @param {String} string
1990
- * @returns {string}
1991
- */
1992
- capitalize:
1993
- (string) =>
1994
- {
1995
- string = STRING(string).trim();
1996
- if(string.length <= 0)
1997
- {
1998
- return string;
1999
- }
2000
- return string.charAt(0).toUpperCase() + string.slice(1);
2001
- },
2002
-
2003
- /**
2004
- * Returns true if the given string ends with any of the given characters or words.
2005
- *
2006
- * @param {string} string
2007
- * @param {string|string[]} endingCharsStringOrArray
2008
- * @returns {boolean}
2009
- */
2010
- endsWithAny:
2011
- (string, endingCharsStringOrArray) =>
2012
- {
2013
- string = STRING(string);
2014
- let endingCharsArray;
2015
- if(Array.isArray(endingCharsStringOrArray))
2016
- {
2017
- endingCharsArray = endingCharsStringOrArray;
2018
- }
2019
- else
2020
- {
2021
- endingCharsArray = STRING(endingCharsStringOrArray).split('');
2022
- }
2023
- let result = false;
2024
- LeUtils.each(endingCharsArray, (chars) =>
2025
- {
2026
- if(string.endsWith(STRING(chars)))
2027
- {
2028
- result = true;
2029
- return false;
2030
- }
2031
- });
2032
- return result;
2033
- },
2034
-
2035
- /**
2036
- * Returns true if the given string starts with any of the given characters or words.
2037
- *
2038
- * @param {string} string
2039
- * @param {string|string[]} startingCharsStringOrArray
2040
- * @returns {boolean}
2041
- */
2042
- startsWithAny:
2043
- (string, startingCharsStringOrArray) =>
2044
- {
2045
- string = STRING(string);
2046
- let startingCharsArray;
2047
- if(Array.isArray(startingCharsStringOrArray))
2048
- {
2049
- startingCharsArray = startingCharsStringOrArray;
2050
- }
2051
- else
2052
- {
2053
- startingCharsArray = STRING(startingCharsStringOrArray).split('');
2054
- }
2055
- let result = false;
2056
- LeUtils.each(startingCharsArray, (chars) =>
2057
- {
2058
- if(string.startsWith(STRING(chars)))
2059
- {
2060
- result = true;
2061
- return false;
2062
- }
2063
- });
2064
- return result;
2065
- },
2066
-
2067
- /**
2068
- * Trims the end of the given string, by removing the given characters from it.
2069
- *
2070
- * @param {string} string
2071
- * @param {string|string[]} trimCharsStringOrArray
2072
- */
2073
- trimEnd:
2074
- (string, trimCharsStringOrArray) =>
2075
- {
2076
- string = STRING(string);
2077
- let endingCharsArray;
2078
- if(Array.isArray(trimCharsStringOrArray))
2079
- {
2080
- endingCharsArray = trimCharsStringOrArray;
2081
- }
2082
- else
2083
- {
2084
- endingCharsArray = STRING(trimCharsStringOrArray).split('');
2085
- }
2086
- const trimChars = (chars) =>
2087
- {
2088
- chars = STRING(chars);
2089
- if(string.endsWith(chars))
2090
- {
2091
- string = string.substring(0, string.length - chars.length);
2092
- run = true;
2093
- }
2094
- };
2095
- let run = true;
2096
- while(run)
2097
- {
2098
- run = false;
2099
- LeUtils.each(endingCharsArray, trimChars);
2100
- }
2101
- return string;
2102
- },
2103
-
2104
- /**
2105
- * Trims the start of the given string, by removing the given characters from it.
2106
- *
2107
- * @param {string} string
2108
- * @param {string|string[]} trimCharsStringOrArray
2109
- */
2110
- trimStart:
2111
- (string, trimCharsStringOrArray) =>
2112
- {
2113
- string = STRING(string);
2114
- let startingCharsArray;
2115
- if(Array.isArray(trimCharsStringOrArray))
2116
- {
2117
- startingCharsArray = trimCharsStringOrArray;
2118
- }
2119
- else
2120
- {
2121
- startingCharsArray = STRING(trimCharsStringOrArray).split('');
2122
- }
2123
- const trimChars = (chars) =>
2124
- {
2125
- chars = STRING(chars);
2126
- if(string.startsWith(chars))
2127
- {
2128
- string = string.substring(chars.length);
2129
- run = true;
2130
- }
2131
- };
2132
- let run = true;
2133
- while(run)
2134
- {
2135
- run = false;
2136
- LeUtils.each(startingCharsArray, trimChars);
2137
- }
2138
- return string;
2139
- },
2140
-
2141
- /**
2142
- * Trims the start and end of the given string, by removing the given characters from it.
2143
- *
2144
- * @param {string} string
2145
- * @param {string|string[]} trimCharsStringOrArray
2146
- */
2147
- trim:
2148
- (string, trimCharsStringOrArray) => LeUtils.trimEnd(LeUtils.trimStart(string, trimCharsStringOrArray), trimCharsStringOrArray),
2149
-
2150
- /**
2151
- * Returns the given string, trims the start and end, and makes sure it ends with a valid sentence ending character (such as !?;.).
2152
- *
2153
- * @param {string} sentence
2154
- * @returns {string}
2155
- */
2156
- purgeSentence:
2157
- (sentence) =>
2158
- {
2159
- sentence = LeUtils.trimEnd(STRING(sentence).trim(), '.: \r\n\t');
2160
- sentence += (LeUtils.endsWithAny(sentence, '!?;') ? '' : '.');
2161
- return sentence;
2162
- },
2163
-
2164
- /**
2165
- * Attempts to obtain and return an error message from the given error, regardless of what is passed to this function.
2166
- *
2167
- * @param {*} error
2168
- * @returns {string}
2169
- */
2170
- purgeErrorMessage:
2171
- (error) =>
2172
- {
2173
- if(error?.message)
2174
- {
2175
- error = error.message;
2176
- }
2177
- if(typeof error === 'string')
2178
- {
2179
- const errorParts = error.split('threw an error:');
2180
- error = errorParts[errorParts.length - 1];
2181
- }
2182
- else
2183
- {
2184
- try
2185
- {
2186
- error = JSON.stringify(error);
2187
- }
2188
- catch(e)
2189
- {
2190
- error = 'An unknown error occurred';
2191
- }
2192
- }
2193
- return error.trim();
2194
- },
2195
-
2196
- /**
2197
- * Generates all permutations of the given names.
2198
- *
2199
- * For example, if you pass "foo" and "bar", it will return:
2200
- * - foobar
2201
- * - fooBar
2202
- * - FooBar
2203
- * - foo-bar
2204
- * - foo_bar
2205
- *
2206
- * @param {string[]} names
2207
- * @returns {string[]}
2208
- */
2209
- generateNamePermutations:
2210
- (...names) =>
2211
- {
2212
- names = LeUtils.flattenToArray(names)
2213
- .map(name => STRING(name).trim().toLowerCase())
2214
- .filter(name => (name.length > 0));
2215
- let results = [];
2216
- if(names.length > 0)
2217
- {
2218
- results.push(names.join('')); //foobar
2219
- results.push(names.map(LeUtils.capitalize).join('')); //FooBar
2220
- }
2221
- if(names.length > 1)
2222
- {
2223
- results.push([names[0]].concat(names.slice(1).map(LeUtils.capitalize)).join('')); //fooBar
2224
- results.push(names.join('-')); //foo-bar
2225
- results.push(names.join('_')); //foo_bar
2226
- }
2227
- return results;
2228
- },
2229
-
2230
- /**
2231
- * Increases the given numeric string by 1, this allows you to increase a numeric string without a limit.
2232
- *
2233
- * @param {string} string
2234
- * @returns {string}
2235
- */
2236
- increaseNumericStringByOne:
2237
- (string) =>
2238
- {
2239
- if(typeof string !== 'string')
2240
- {
2241
- string = '' + string;
2242
- for(let i = string.length - 1; i >= 0; i--)
2243
- {
2244
- const c = string.charAt(i);
2245
- if((c < '0') || (c > '9'))
2246
- {
2247
- return '1';
2248
- }
2249
- }
2250
- }
2251
- if(string === '')
2252
- {
2253
- return '1';
2254
- }
2255
- for(let i = string.length - 1; i >= 0; i--)
2256
- {
2257
- let c = string.charAt(i);
2258
- if((c < '0') || (c > '9'))
2259
- {
2260
- return '1';
2261
- }
2262
- if(c < '9')
2263
- {
2264
- c = String.fromCharCode(c.charCodeAt(0) + 1);
2265
- string = string.substring(0, i) + c + string.substring(i + 1);// string[i] = (char + 1);
2266
- break;
2267
- }
2268
- string = string.substring(0, i) + '0' + string.substring(i + 1);// string[i] = '0';
2269
- }
2270
- if(string.charAt(0) === '0')
2271
- {
2272
- string = '1' + string;
2273
- }
2274
- return string;
2275
- },
2276
-
2277
- /**
2278
- * Generates a base64 string (with +/ replaced by -_) that is guaranteed to be unique.
2279
- *
2280
- * @returns {string}
2281
- */
2282
- uniqueId:
2283
- (() =>
2284
- {
2285
- let previousUniqueIdsTime = null;
2286
- let previousUniqueIds = new Map();
2287
-
2288
- const numberToBytes = (number) =>
2289
- {
2290
- const size = (number === 0) ? 0 : Math.ceil((Math.floor(Math.log2(number)) + 1) / 8);
2291
- const bytes = new Uint8ClampedArray(size);
2292
- let x = number;
2293
- for(let i = (size - 1); i >= 0; i--)
2294
- {
2295
- const rightByte = x & 0xff;
2296
- bytes[i] = rightByte;
2297
- x = Math.floor(x / 0x100);
2298
- }
2299
- return bytes;
2300
- };
2301
-
2302
- const generateUniqueId = () =>
2303
- {
2304
- let now;
2305
- try
2306
- {
2307
- // noinspection JSDeprecatedSymbols
2308
- now = (globalThis?.performance?.timeOrigin || globalThis?.performance?.timing?.navigationStart || 0) + (globalThis?.performance?.now?.() ?? 0);
2309
- }
2310
- catch(e)
2311
- {
2312
- }
2313
- now = now || (Date.now ? Date.now() : (new Date()).getTime());
2314
- now = Math.round(now);
2315
- const nowBytes = numberToBytes(now);
2316
-
2317
- let uuid = null;
2318
- try
2319
- {
2320
- uuid = crypto?.randomUUID();
2321
- }
2322
- catch(e)
2323
- {
2324
- }
2325
-
2326
- if(uuid)
2327
- {
2328
- uuid = LeUtils.base64ToBytes(LeUtils.hexToBase64(uuid));
2329
- }
2330
- else
2331
- {
2332
- const bytesChunkA = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0'));
2333
- const bytesChunkB = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0'));
2334
- const bytesChunkC = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0'));
2335
- const bytesChunkD = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0'));
2336
- uuid = new Uint8Array(bytesChunkA.length + bytesChunkB.length + bytesChunkC.length + bytesChunkD.length);
2337
- uuid.set(bytesChunkA, 0);
2338
- uuid.set(bytesChunkB, bytesChunkA.length);
2339
- uuid.set(bytesChunkC, bytesChunkA.length + bytesChunkB.length);
2340
- uuid.set(bytesChunkD, bytesChunkA.length + bytesChunkB.length + bytesChunkC.length);
2341
- }
2342
-
2343
- const bytes = new Uint8Array(nowBytes.length + uuid.length);
2344
- bytes.set(nowBytes, 0);
2345
- bytes.set(uuid, nowBytes.length);
2346
- uuid = LeUtils.bytesToBase64(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
2347
-
2348
- return {
2349
- time:now,
2350
- id: uuid,
2351
- };
2352
- };
2353
-
2354
- return () =>
2355
- {
2356
- while(true)
2357
- {
2358
- const result = generateUniqueId();
2359
- if(previousUniqueIdsTime !== result.time)
2360
- {
2361
- previousUniqueIdsTime = result.time;
2362
- previousUniqueIds.clear();
2363
- previousUniqueIds.set(result.id, true);
2364
- return result.id;
2365
- }
2366
- else if(previousUniqueIds.get(result.id) !== true)
2367
- {
2368
- previousUniqueIds.set(result.id, true);
2369
- return result.id;
2370
- }
2371
- }
2372
- };
2373
- })(),
2374
-
2375
- /**
2376
- * Generates a base64 string (with +/ replaced by -_) of the current time (in milliseconds since 1970).
2377
- *
2378
- * @param {number} [now] Optional time to use instead of the current time. If not set, the current time will be used.
2379
- * @returns {string}
2380
- */
2381
- timestamp:
2382
- (() =>
2383
- {
2384
- const numberToBytes = (number) =>
2385
- {
2386
- const size = (number === 0) ? 0 : Math.ceil((Math.floor(Math.log2(number)) + 1) / 8);
2387
- const bytes = new Uint8ClampedArray(size);
2388
- let x = number;
2389
- for(let i = (size - 1); i >= 0; i--)
2390
- {
2391
- const rightByte = x & 0xff;
2392
- bytes[i] = rightByte;
2393
- x = Math.floor(x / 0x100);
2394
- }
2395
- return bytes;
2396
- };
2397
-
2398
- return (/** @type {number|null|undefined} */ now = null) =>
2399
- {
2400
- if(ISSET(now))
2401
- {
2402
- now = FLOAT_LAX(now);
2403
- }
2404
- else
2405
- {
2406
- try
2407
- {
2408
- // noinspection JSDeprecatedSymbols
2409
- now = (performance?.timeOrigin || performance?.timing?.navigationStart || 0) + (performance?.now?.() ?? 0);
2410
- }
2411
- catch(e)
2412
- {
2413
- }
2414
- now = now || (Date.now ? Date.now() : (new Date()).getTime());
2415
- }
2416
- now = Math.round(now);
2417
- const nowBytes = numberToBytes(now);
2418
-
2419
- return LeUtils.bytesToBase64(nowBytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
2420
- };
2421
- })(),
2422
-
2423
- /**
2424
- * Returns a data URL of a 1x1 transparent pixel.
2425
- *
2426
- * @returns {string}
2427
- */
2428
- getEmptyImageSrc:
2429
- () =>
2430
- {
2431
- // noinspection SpellCheckingInspection
2432
- return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
2433
- },
2434
-
2435
- /**
2436
- * Calculates and returns the percentage of the part and total ((part / total) * 100).
2437
- *
2438
- * @param {number|string} part
2439
- * @param {number|string} total
2440
- * @returns {number}
2441
- */
2442
- getPercentage:
2443
- (part, total) =>
2444
- {
2445
- part = FLOAT_LAX(part);
2446
- total = FLOAT_LAX(total);
2447
- if(total === 0)
2448
- {
2449
- return 100;
2450
- }
2451
- return Math.max(0, Math.min(100, ((part / total) * 100)));
2452
- },
2453
-
2454
- /**
2455
- * Returns the pixels of the given Image object.
2456
- *
2457
- * @param {HTMLImageElement} image
2458
- * @returns {Uint8ClampedArray}
2459
- */
2460
- getImagePixels:
2461
- (image) =>
2462
- {
2463
- if(!globalThis?.document?.createElement || !globalThis?.document?.body?.appendChild)
2464
- {
2465
- console.warn('LeUtils.getImagePixels: Document is not available, returning empty pixels.');
2466
- return new Uint8ClampedArray();
2467
- }
2468
- const canvas = globalThis.document.createElement('canvas');
2469
- globalThis.document.body.appendChild(canvas);
2470
- try
2471
- {
2472
- const ctx = canvas.getContext('2d');
2473
- const width = Math.floor(image.width);
2474
- const height = Math.floor(image.height);
2475
- if(!ctx || (width <= 0) || (height <= 0))
2476
- {
2477
- return new Uint8ClampedArray();
2478
- }
2479
- canvas.width = width;
2480
- canvas.height = height;
2481
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
2482
- return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
2483
- }
2484
- finally
2485
- {
2486
- canvas?.parentNode?.removeChild(canvas);
2487
- }
2488
- },
2489
-
2490
- /**
2491
- * Returns the data URL (mimetype "image/png") of a colored version of the given Image object.
2492
- *
2493
- * @param {HTMLImageElement} image
2494
- * @param {string} color
2495
- * @returns {string}
2496
- */
2497
- getColoredImage:
2498
- (image, color) =>
2499
- {
2500
- if(!globalThis?.document?.createElement || !globalThis?.document?.body?.appendChild)
2501
- {
2502
- console.warn('LeUtils.getColoredImage: Document is not available, returning empty image src.');
2503
- return LeUtils.getEmptyImageSrc();
2504
- }
2505
- const canvas = globalThis.document.createElement('canvas');
2506
- globalThis.document.body.appendChild(canvas);
2507
- try
2508
- {
2509
- const ctx = canvas.getContext('2d');
2510
- const width = Math.floor(image.width);
2511
- const height = Math.floor(image.height);
2512
- if(!ctx || (width <= 0) || (height <= 0))
2513
- {
2514
- return LeUtils.getEmptyImageSrc();
2515
- }
2516
- canvas.width = width;
2517
- canvas.height = height;
2518
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
2519
- ctx.globalCompositeOperation = 'source-in';
2520
- ctx.fillStyle = color;
2521
- ctx.fillRect(0, 0, canvas.width, canvas.height);
2522
- return canvas.toDataURL('image/png');
2523
- }
2524
- finally
2525
- {
2526
- canvas?.parentNode?.removeChild(canvas);
2527
- }
2528
- },
2529
-
2530
- /**
2531
- * Returns the hex color of the given RGB(A).
2532
- *
2533
- * @param {number[]} rgb
2534
- * @returns {string}
2535
- */
2536
- rgbToHex:
2537
- (rgb) =>
2538
- {
2539
- return '#' + rgb.map((x) =>
2540
- {
2541
- const hex = x.toString(16);
2542
- return ((hex.length === 1) ? '0' + hex : hex);
2543
- }).join('');
2544
- },
2545
-
2546
- /**
2547
- * Returns the RGB(A) of the given hex color.
2548
- *
2549
- * @param {string} hexstring
2550
- * @returns {number[]}
2551
- */
2552
- hexToRgb:
2553
- (hexstring) =>
2554
- {
2555
- const initialHexstring = hexstring;
2556
- hexstring = hexstring.replace(/[^0-9A-F]/gi, '');
2557
- const hasAlpha = ((hexstring.length === 4) || (hexstring.length === 8));
2558
- while(hexstring.length < 6)
2559
- {
2560
- hexstring = hexstring.replace(/(.)/g, '$1$1');
2561
- }
2562
- const result = hexstring.match(/\w{2}/g)?.map((a) => parseInt(a, 16));
2563
- if(!result || (result.length < 3))
2564
- {
2565
- throw new Error('Invalid hex color: "' + hexstring + '" (was given "' + initialHexstring + '")');
2566
- }
2567
- return [
2568
- result[0],
2569
- result[1],
2570
- result[2],
2571
- ...(hasAlpha ? [result[3]] : []),
2572
- ];
2573
- },
2574
-
2575
- /**
2576
- * Returns the HSL(A) of the given RGB(A).
2577
- *
2578
- * @param {number[]} rgb
2579
- * @returns {number[]}
2580
- */
2581
- rgbToHsl:
2582
- (rgb) =>
2583
- {
2584
- const r = rgb[0] / 255;
2585
- const g = rgb[1] / 255;
2586
- const b = rgb[2] / 255;
2587
- const max = Math.max(r, g, b);
2588
- const min = Math.min(r, g, b);
2589
- let h = 0;
2590
- let s = 0;
2591
- let l = (max + min) / 2;
2592
- if(max !== min)
2593
- {
2594
- const d = max - min;
2595
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
2596
- switch(max)
2597
- {
2598
- case r:
2599
- h = (g - b) / d + (g < b ? 6 : 0);
2600
- break;
2601
- case g:
2602
- h = (b - r) / d + 2;
2603
- break;
2604
- case b:
2605
- h = (r - g) / d + 4;
2606
- break;
2607
- }
2608
- h /= 6;
2609
- }
2610
- return [h, s, l, ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
2611
- },
2612
-
2613
- /**
2614
- * Returns the RGB(A) of the given HSL(A).
2615
- *
2616
- * @param {number[]} hsl
2617
- * @returns {number[]}
2618
- */
2619
- hslToRgb:
2620
- (() =>
2621
- {
2622
- const hue2rgb = (p, q, t) =>
2623
- {
2624
- if(t < 0)
2625
- {
2626
- t += 1;
2627
- }
2628
- if(t > 1)
2629
- {
2630
- t -= 1;
2631
- }
2632
- if(t < 1 / 6)
2633
- {
2634
- return p + (q - p) * 6 * t;
2635
- }
2636
- if(t < 1 / 2)
2637
- {
2638
- return q;
2639
- }
2640
- if(t < 2 / 3)
2641
- {
2642
- return p + (q - p) * (2 / 3 - t) * 6;
2643
- }
2644
- return p;
2645
- };
2646
- return (hsl) =>
2647
- {
2648
- const h = hsl[0];
2649
- const s = hsl[1];
2650
- const l = hsl[2];
2651
- let r = 1;
2652
- let g = 1;
2653
- let b = 1;
2654
- if(s !== 0)
2655
- {
2656
- const q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s));
2657
- const p = (2 * l) - q;
2658
- r = hue2rgb(p, q, h + (1 / 3));
2659
- g = hue2rgb(p, q, h);
2660
- b = hue2rgb(p, q, h - (1 / 3));
2661
- }
2662
- return [r * 255, g * 255, b * 255, ...((hsl.length >= 4) ? [hsl[3] * 255] : [])].map((c) => Math.max(0, Math.min(255, Math.round(c))));
2663
- };
2664
- })(),
2665
-
2666
- /**
2667
- * Returns the LAB(A) of the given RGB(A).
2668
- *
2669
- * @param {number[]} rgb
2670
- * @returns {number[]}
2671
- */
2672
- rgbToLab:
2673
- (rgb) =>
2674
- {
2675
- let r = rgb[0] / 255;
2676
- let g = rgb[1] / 255;
2677
- let b = rgb[2] / 255;
2678
- r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
2679
- g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
2680
- b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
2681
- let x = ((r * 0.4124) + (g * 0.3576) + (b * 0.1805)) / 0.95047;
2682
- let y = ((r * 0.2126) + (g * 0.7152) + (b * 0.0722));
2683
- let z = ((r * 0.0193) + (g * 0.1192) + (b * 0.9505)) / 1.08883;
2684
- x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);
2685
- y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);
2686
- z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);
2687
- return [(116 * y) - 16, 500 * (x - y), 200 * (y - z), ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
2688
- },
2689
-
2690
- /**
2691
- * Returns the difference (calculated with DeltaE) of the LAB values of the given RGB values.
2692
- *
2693
- * Returns a number:
2694
- *
2695
- * ```js
2696
- * < 1 is not perceptible by human eyes
2697
- * 1-2 is perceptible through close observation
2698
- * 2-10 is perceptible at a glance
2699
- * 11-49 is more similar than opposite
2700
- * 100 is exactly the opposite color
2701
- * ```
2702
- *
2703
- * @param {number[]} rgbA
2704
- * @param {number[]} rgbB
2705
- * @returns {number}
2706
- */
2707
- getDifferenceBetweenRgb:
2708
- (rgbA, rgbB) =>
2709
- {
2710
- const labA = LeUtils.rgbToLab(rgbA);
2711
- const labB = LeUtils.rgbToLab(rgbB);
2712
- return LeUtils.getDifferenceBetweenLab(labA, labB);
2713
- },
2714
-
2715
- /**
2716
- * Returns the difference (calculated with DeltaE) of the given LAB values. Ignores the Alpha channel.
2717
- *
2718
- * Returns a number:
2719
- *
2720
- * ```js
2721
- * < 1 is not perceptible by human eyes
2722
- * 1-2 is perceptible through close observation
2723
- * 2-10 is perceptible at a glance
2724
- * 11-49 is more similar than opposite
2725
- * 100 is exactly the opposite color
2726
- * ```
2727
- *
2728
- * @param {number[]} labA
2729
- * @param {number[]} labB
2730
- * @returns {number}
2731
- */
2732
- getDifferenceBetweenLab:
2733
- (labA, labB) =>
2734
- {
2735
- const deltaL = labA[0] - labB[0];
2736
- const deltaA = labA[1] - labB[1];
2737
- const deltaB = labA[2] - labB[2];
2738
- const c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
2739
- const c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
2740
- const deltaC = c1 - c2;
2741
- let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
2742
- deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
2743
- const sc = 1.0 + 0.045 * c1;
2744
- const sh = 1.0 + 0.015 * c1;
2745
- const deltaLKlsl = deltaL / (1.0);
2746
- const deltaCkcsc = deltaC / (sc);
2747
- const deltaHkhsh = deltaH / (sh);
2748
- const i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
2749
- return (i < 0) ? 0 : Math.sqrt(i);
2750
- },
2751
-
2752
- /**
2753
- * Returns the RGB(A) of the given RGB(A) values, based on the given percentage (0-100).
2754
- * This allows you to define a gradient of colors to fade in between, rather than just having a start and an end color.
2755
- *
2756
- * Usage:
2757
- *
2758
- * ```js
2759
- * LeUtils.getRgbOfGradient({
2760
- * 0: [255, 0, 0],
2761
- * 33: [255, 255, 0],
2762
- * 66: [0, 255, 0],
2763
- * 100:[0, 255, 255],
2764
- * }, 45.1234);
2765
- * ```
2766
- *
2767
- * @param {{percentage?:number[]}} gradient
2768
- * @param {number} percentage
2769
- * @returns {number[]}
2770
- */
2771
- getRgbOfGradient:
2772
- (gradient, percentage) =>
2773
- {
2774
- percentage = Math.max(0, Math.min(100, FLOAT_LAX(percentage)));
2775
-
2776
- let closest = null;
2777
- LeUtils.each(gradient, (color, percent) =>
2778
- {
2779
- percent = INT_LAX(percent);
2780
- if(closest === null)
2781
- {
2782
- closest = [percent, Math.abs(percentage - percent)];
2783
- }
2784
- else
2785
- {
2786
- const difference = Math.abs(percentage - percent);
2787
- if(difference < closest[1])
2788
- {
2789
- closest = [percent, difference];
2790
- }
2791
- }
2792
- });
2793
- if(closest === null)
2794
- {
2795
- return [0, 0, 0];
2796
- }
2797
- closest = closest[0];
2798
-
2799
- const HIGHER = 99999;
2800
- const LOWER = -99999;
2801
- let higher = HIGHER;
2802
- let lower = LOWER;
2803
- LeUtils.each(gradient, (color, percent) =>
2804
- {
2805
- percent = INT_LAX(percent);
2806
- if(percent < closest)
2807
- {
2808
- if(percent > lower)
2809
- {
2810
- lower = percent;
2811
- }
2812
- }
2813
- if(percent > closest)
2814
- {
2815
- if(percent < higher)
2816
- {
2817
- higher = percent;
2818
- }
2819
- }
2820
- });
2821
-
2822
- if(((higher === HIGHER) && (lower === LOWER)) || (higher === lower))
2823
- {
2824
- return gradient[closest];
2825
- }
2826
- else if((higher !== HIGHER) && (lower !== LOWER))
2827
- {
2828
- const higherDifference = Math.abs(higher - percentage);
2829
- const lowerDifference = Math.abs(percentage - lower);
2830
- if(higherDifference > lowerDifference)
2831
- {
2832
- higher = closest;
2833
- }
2834
- else
2835
- {
2836
- lower = closest;
2837
- }
2838
- }
2839
- else if(lower === LOWER)
2840
- {
2841
- lower = closest;
2842
- }
2843
- else
2844
- {
2845
- higher = closest;
2846
- }
2847
-
2848
- if(lower > higher)
2849
- {
2850
- const tmp = higher;
2851
- higher = lower;
2852
- lower = tmp;
2853
- }
2854
-
2855
- const total = (higher - lower);
2856
- const part = (percentage - lower);
2857
- return LeUtils.getRgbBetween(gradient[lower], gradient[higher], ((part / total) * 100));
2858
- },
2859
-
2860
- /**
2861
- * Returns the RGB(A) between the two given RGB(A) values, based on the given percentage (0-100).
2862
- *
2863
- * @param {number[]} startRgb
2864
- * @param {number[]} endRgb
2865
- * @param {number} percentage
2866
- * @returns {number[]}
2867
- */
2868
- getRgbBetween:
2869
- (startRgb, endRgb, percentage) =>
2870
- {
2871
- percentage = FLOAT_LAX(percentage);
2872
- const partEnd = Math.max(0, Math.min(1, (percentage / 100.0)));
2873
- const partStart = (1 - partEnd);
2874
- const length = Math.min(startRgb.length, endRgb.length);
2875
- let result = [];
2876
- for(let i = 0; i < length; i++)
2877
- {
2878
- result.push(Math.max(0, Math.min(255, Math.round((startRgb[i] * partStart) + (endRgb[i] * partEnd)))));
2879
- }
2880
- return result;
2881
- },
2882
-
2883
- /**
2884
- * An implementation of the btoa function, which should work in all environments.
2885
- *
2886
- * @param {string} string
2887
- * @returns {string}
2888
- */
2889
- btoa:
2890
- (string) =>
2891
- {
2892
- if(typeof globalThis?.btoa === 'function')
2893
- {
2894
- return globalThis.btoa(string);
2895
- }
2896
- if(typeof globalThis?.Buffer?.from === 'function')
2897
- {
2898
- return globalThis.Buffer.from(string).toString('base64');
2899
- }
2900
- throw new Error('LeUtils.btoa: No btoa implementation found in this environment.');
2901
- },
2902
-
2903
- /**
2904
- * An implementation of the atob function, which should work in all environments.
2905
- *
2906
- * @param {string} base64string
2907
- * @returns {string}
2908
- */
2909
- atob:
2910
- (base64string) =>
2911
- {
2912
- if(typeof globalThis?.atob === 'function')
2913
- {
2914
- return globalThis.atob(base64string);
2915
- }
2916
- if(typeof globalThis?.Buffer?.from === 'function')
2917
- {
2918
- return globalThis.Buffer.from(base64string, 'base64').toString();
2919
- }
2920
- throw new Error('LeUtils.atob: No atob implementation found in this environment.');
2921
- },
2922
-
2923
- /**
2924
- * Encodes a UTF-8 string into a base64 string.
2925
- *
2926
- * @param {string} string
2927
- * @returns {string}
2928
- */
2929
- utf8ToBase64:
2930
- (string) =>
2931
- {
2932
- return LeUtils.btoa(encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16))));
2933
- },
2934
-
2935
- /**
2936
- * Decodes a base64 string back into a UTF-8 string.
2937
- *
2938
- * @param {string} base64string
2939
- * @returns {string}
2940
- */
2941
- base64ToUtf8:
2942
- (base64string) =>
2943
- {
2944
- return decodeURIComponent(LeUtils.atob(base64string.trim()).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
2945
- },
2946
-
2947
- /**
2948
- * Converts a base64 string into a hex string.
2949
- *
2950
- * @param {string} base64string
2951
- * @returns {string}
2952
- */
2953
- base64ToHex:
2954
- (base64string) =>
2955
- {
2956
- return LeUtils.atob(base64string.trim()).split('').map((c) => ('0' + c.charCodeAt(0).toString(16)).slice(-2)).join('');
2957
- },
2958
-
2959
- /**
2960
- * Converts a hex string into a base64 string.
2961
- *
2962
- * @param {string} hexstring
2963
- * @returns {string}
2964
- */
2965
- hexToBase64:
2966
- (hexstring) =>
2967
- {
2968
- const hexResult = hexstring.replace(/[^0-9A-F]/gi, '').match(/\w{2}/g)?.map((a) => String.fromCharCode(parseInt(a, 16)))?.join('');
2969
- if(!hexResult)
2970
- {
2971
- throw new Error('Invalid hex string: "' + hexstring + '"');
2972
- }
2973
- return LeUtils.btoa(hexResult);
2974
- },
2975
-
2976
- /**
2977
- * Converts a base64 string into bytes (Uint8Array).
2978
- *
2979
- * @param {string} base64string
2980
- * @returns {Uint8Array}
2981
- */
2982
- base64ToBytes:
2983
- (base64string) =>
2984
- {
2985
- return Uint8Array.from(LeUtils.atob(base64string.trim()), c => c.charCodeAt(0));
2986
- },
2987
-
2988
- /**
2989
- * Converts bytes into a base64 string.
2990
- *
2991
- * @param {ArrayLike<number>|ArrayBuffer} arraybuffer
2992
- * @returns {string}
2993
- */
2994
- bytesToBase64:
2995
- (arraybuffer) =>
2996
- {
2997
- return LeUtils.btoa(String.fromCharCode(...new Uint8Array(arraybuffer)));
2998
- },
2999
-
3000
- /**
3001
- * Downloads the given base64 string as a file.
3002
- *
3003
- * @param {string} base64string
3004
- * @param {string} [fileName]
3005
- * @param {string} [mimeType]
3006
- */
3007
- downloadFile:
3008
- (base64string, fileName, mimeType) =>
3009
- {
3010
- if(!globalThis?.document?.createElement)
3011
- {
3012
- console.warn('LeUtils.downloadFile: Document is not available, cannot download file.');
3013
- return;
3014
- }
3015
- const link = globalThis.document.createElement('a');
3016
- link.setAttribute('download', (typeof fileName === 'string') ? fileName : 'file');
3017
- link.href = 'data:' + mimeType + ';base64,' + base64string;
3018
- link.setAttribute('target', '_blank');
3019
- link.click();
3020
- },
3021
-
3022
- /**
3023
- * Loads the value from the browser, returns undefined if the value doesn't exist.
3024
- *
3025
- * @param {string} id
3026
- * @returns {*}
3027
- */
3028
- localStorageGet:
3029
- (id) =>
3030
- {
3031
- if(!globalThis?.localStorage?.getItem)
3032
- {
3033
- console.warn('LeUtils.localStorageGet: LocalStorage is not available, returning undefined.');
3034
- return;
3035
- }
3036
- let result = globalThis.localStorage.getItem('LeUtils_' + id);
3037
- if(typeof result !== 'string')
3038
- {
3039
- return;
3040
- }
3041
- try
3042
- {
3043
- return JSON.parse(result)?.['-'];
3044
- }
3045
- catch(e)
3046
- {
3047
- }
3048
- },
3049
-
3050
- /**
3051
- * Saves the given data in the browser.
3052
- *
3053
- * @param {string} id
3054
- * @param {*} data
3055
- */
3056
- localStorageSet:
3057
- (id, data) =>
3058
- {
3059
- if(!globalThis?.localStorage?.setItem)
3060
- {
3061
- console.warn('LeUtils.localStorageSet: LocalStorage is not available, cannot save data.');
3062
- return;
3063
- }
3064
- if(typeof data === 'undefined')
3065
- {
3066
- globalThis.localStorage.removeItem('LeUtils_' + id);
3067
- return;
3068
- }
3069
- globalThis.localStorage.setItem('LeUtils_' + id, JSON.stringify({'-':data}));
3070
- },
3071
-
3072
- /**
3073
- * Removes the data from the browser.
3074
- *
3075
- * @param {string} id
3076
- */
3077
- localStorageRemove:
3078
- (id) =>
3079
- {
3080
- if(!globalThis?.localStorage?.removeItem)
3081
- {
3082
- console.warn('LeUtils.localStorageRemove: LocalStorage is not available, cannot remove data.');
3083
- return;
3084
- }
3085
- globalThis.localStorage.removeItem('LeUtils_' + id);
3086
- },
3087
-
3088
- /**
3089
- * Returns whether the current hostname (globalThis.location.hostname) is private (such as localhost, 192.168.1.1, etc).
3090
- * This can be used to determine if the app is running in a development environment or not.
3091
- *
3092
- * Only "localhost" and IPv4 addresses are supported. IPv6 addresses will always return false.
3093
- *
3094
- * @returns {boolean}
3095
- */
3096
- isCurrentHostPrivate:
3097
- (() =>
3098
- {
3099
- let lastHostname = null;
3100
- let lastResult = false;
3101
-
3102
- return () =>
3103
- {
3104
- if(!globalThis?.location?.hostname)
3105
- {
3106
- console.warn('LeUtils.isCurrentHostPrivate: No location.hostname found, returning false.');
3107
- return false;
3108
- }
3109
- const hostname = globalThis.location.hostname;
3110
- if(lastHostname === hostname)
3111
- {
3112
- return lastResult;
3113
- }
3114
- lastHostname = hostname;
3115
- lastResult = LeUtils.isGivenHostPrivate(lastHostname);
3116
- return lastResult;
3117
- };
3118
- })(),
3119
-
3120
- /**
3121
- * Returns true if the given hostname is private (such as localhost, 192.168.1.1, etc).
3122
- *
3123
- * Only "localhost" and IPv4 addresses are supported. IPv6 addresses will always return false.
3124
- *
3125
- * @param {string} host
3126
- * @returns {boolean}
3127
- */
3128
- isGivenHostPrivate:
3129
- (host) =>
3130
- {
3131
- host = STRING(host).trim();
3132
- if(!host)
3133
- {
3134
- return false;
3135
- }
3136
- try
3137
- {
3138
- host = (new URL(host)).hostname;
3139
- }
3140
- catch(e)
3141
- {
3142
- host = host.split(':')[0];
3143
- }
3144
- host = STRING(host).trim().toLowerCase();
3145
-
3146
- if((host === 'localhost') || (host === '127.0.0.1'))
3147
- {
3148
- return true;
3149
- }
3150
- if(!/^(\d{1,3}\.){3}\d{1,3}$/.test(host))
3151
- {
3152
- return false;
3153
- }
3154
- const parts = host.split('.');
3155
- return (parts[0] === '10') || // 10.0.0.0 - 10.255.255.255
3156
- ((parts[0] === '172') && ((parseInt(parts[1], 10) >= 16) && (parseInt(parts[1], 10) <= 31))) || // 172.16.0.0 - 172.31.255.255
3157
- ((parts[0] === '192') && (parts[1] === '168')); // 192.168.0.0 - 192.168.255.255
3158
- },
3159
-
3160
- /**
3161
- * @typedef {Object} LeUtils_TransactionalValue
3162
- * @property {*} value
3163
- * @property {{id:string, value:*}[]} changes
3164
- */
3165
- /**
3166
- * Creates and returns a new TransactionalValue object.
3167
- * With a TransactionalValue, you can keep track of changes to a value, and commit or cancel them.
3168
- *
3169
- * Multiple uncommitted changes can be made at the same time, the last change will be the one that overwrites older changes.
3170
- * If that change is cancelled, the previous change will be the one that overwrites older changes.
3171
- * This allows you to make multiple unconfirmed changes, and confirm or cancel each of them individually at any time.
3172
- *
3173
- * @param {*} [value]
3174
- * @returns {LeUtils_TransactionalValue}
3175
- */
3176
- createTransactionalValue:
3177
- (value) =>
3178
- {
3179
- if(typeof value === 'undefined')
3180
- {
3181
- value = null;
3182
- }
3183
- return {value:value, changes:[]};
3184
- },
3185
-
3186
- /**
3187
- * Returns true if the given value is a valid TransactionalValue, returns false if it isn't.
3188
- *
3189
- * @param {LeUtils_TransactionalValue} transactionalValue
3190
- * @returns {boolean}
3191
- */
3192
- isTransactionalValueValid:
3193
- (transactionalValue) =>
3194
- {
3195
- return ((typeof transactionalValue === 'object') && ('value' in transactionalValue) && ('changes' in transactionalValue) && Array.isArray(transactionalValue.changes));
3196
- },
3197
-
3198
- /**
3199
- * Returns true if the given value is a TransactionalValue, false otherwise.
3200
- *
3201
- * @param {LeUtils_TransactionalValue} transactionalValue
3202
- * @returns {string}
3203
- */
3204
- transactionalValueToString:
3205
- (transactionalValue) =>
3206
- {
3207
- if(!LeUtils.isTransactionalValueValid(transactionalValue))
3208
- {
3209
- return transactionalValue + '';
3210
- }
3211
- if(transactionalValue.changes.length <= 0)
3212
- {
3213
- return '' + transactionalValue.value;
3214
- }
3215
- let valuesString = '' + transactionalValue.value;
3216
- for(let i = 0; i < transactionalValue.changes.length; i++)
3217
- {
3218
- valuesString += ' -> ' + transactionalValue.changes[i].value;
3219
- }
3220
- return transactionalValue.changes[transactionalValue.changes.length - 1].value + ' (' + valuesString + ')';
3221
- },
3222
-
3223
- /**
3224
- * Sets the committed value of the given TransactionalValue to the given value. Clears out the previously uncommitted changes.
3225
- *
3226
- * @param {LeUtils_TransactionalValue} transactionalValue
3227
- * @param {*} value
3228
- */
3229
- transactionSetAndCommit:
3230
- (transactionalValue, value) =>
3231
- {
3232
- checkTransactionalValue(transactionalValue);
3233
- if(typeof value === 'undefined')
3234
- {
3235
- value = null;
3236
- }
3237
- transactionalValue.value = value;
3238
- transactionalValue.changes = [];
3239
- },
3240
-
3241
- /**
3242
- * Sets the value of the given TransactionalValue to the given value, without yet committing it, meaning it can be committed or cancelled later.
3243
- * It returns the ID of the change, which can be used to commit or cancel the change later.
3244
- *
3245
- * @param {LeUtils_TransactionalValue} transactionalValue
3246
- * @param {*} value
3247
- * @returns {string}
3248
- */
3249
- transactionSetWithoutCommitting:
3250
- (transactionalValue, value) =>
3251
- {
3252
- checkTransactionalValue(transactionalValue);
3253
- if(typeof value === 'undefined')
3254
- {
3255
- value = null;
3256
- }
3257
- const id = LeUtils.uniqueId();
3258
- transactionalValue.changes.push({id:id, value:value});
3259
- return id;
3260
- },
3261
-
3262
- /**
3263
- * Commits the change with the given ID, making it the new committed value.
3264
- * Returns true if the change was found and committed, returns false if it was already overwritten by a newer committed change.
3265
- *
3266
- * @param {LeUtils_TransactionalValue} transactionalValue
3267
- * @param {string} changeId
3268
- * @returns {boolean}
3269
- */
3270
- transactionCommitChange:
3271
- (transactionalValue, changeId) =>
3272
- {
3273
- checkTransactionalValue(transactionalValue);
3274
- const change = findTransactionalValueChange(transactionalValue, changeId);
3275
- if(change === null)
3276
- {
3277
- return false;
3278
- }
3279
- transactionalValue.value = change.value;
3280
- transactionalValue.changes.splice(0, change.index + 1);
3281
- return true;
3282
- },
3283
-
3284
- /**
3285
- * Cancels the change with the given ID, removing it from the uncommitted changes.
3286
- * Returns true if the change was found and removed, returns false if it was already overwritten by a newer committed change.
3287
- *
3288
- * @param {LeUtils_TransactionalValue} transactionalValue
3289
- * @param {string} changeId
3290
- * @returns {boolean}
3291
- */
3292
- transactionCancelChange:
3293
- (transactionalValue, changeId) =>
3294
- {
3295
- checkTransactionalValue(transactionalValue);
3296
- const change = findTransactionalValueChange(transactionalValue, changeId);
3297
- if(change === null)
3298
- {
3299
- return false;
3300
- }
3301
- transactionalValue.changes.splice(change.index, 1);
3302
- return true;
3303
- },
3304
-
3305
- /**
3306
- * Returns true if the change was found, meaning it can still make a difference to the final committed value of this TransactionalValue.
3307
- * Returns false if it was already overwritten by a newer committed change, meaning that this change can no longer make a difference to the final committed value of this TransactionalValue.
3308
- *
3309
- * @param {LeUtils_TransactionalValue} transactionalValue
3310
- * @param {string} changeId
3311
- * @returns {boolean}
3312
- */
3313
- transactionIsChangeRelevant:
3314
- (transactionalValue, changeId) =>
3315
- {
3316
- checkTransactionalValue(transactionalValue);
3317
- return (findTransactionalValueChange(transactionalValue, changeId) !== null);
3318
- },
3319
-
3320
- /**
3321
- * Returns the committed value of the given TransactionalValue.
3322
- *
3323
- * @param {LeUtils_TransactionalValue} transactionalValue
3324
- * @returns {*}
3325
- */
3326
- transactionGetCommittedValue:
3327
- (transactionalValue) =>
3328
- {
3329
- checkTransactionalValue(transactionalValue);
3330
- return transactionalValue.value;
3331
- },
3332
-
3333
- /**
3334
- * Returns the value (including any uncommitted changes made to it) of the given TransactionalValue.
3335
- *
3336
- * @param {LeUtils_TransactionalValue} transactionalValue
3337
- * @returns {*}
3338
- */
3339
- transactionGetValue:
3340
- (transactionalValue) =>
3341
- {
3342
- checkTransactionalValue(transactionalValue);
3343
- if(transactionalValue.changes.length <= 0)
3344
- {
3345
- return transactionalValue.value;
3346
- }
3347
- return transactionalValue.changes[transactionalValue.changes.length - 1].value;
3348
- },
3349
-
3350
- /**
3351
- * Creates a worker thread. Workers have to be stored at /workers/{workerName}.worker.js for this to work.
3352
- *
3353
- * Example of a worker file:
3354
- *
3355
- * ```js
3356
- * onmessage = (message) =>
3357
- * {
3358
- * postMessage({
3359
- * ...message.data,
3360
- * results: ['...some expensive calculation involving message.data...'],
3361
- * });
3362
- * };
3363
- * ```
3364
- *
3365
- * Usage:
3366
- *
3367
- * ```js
3368
- * const {results} = await (async () =>
3369
- * {
3370
- * try
3371
- * {
3372
- * return await LeUtils.sendWorkerMessage('my-worker', {someData:[1, 2, 3, 4, 5]});
3373
- * }
3374
- * catch(error)
3375
- * {
3376
- * console.error('MyWorker: ', error);
3377
- * return {results:[]};
3378
- * }
3379
- * })();
3380
- * ```
3381
- *
3382
- * or, if you want more control over the number of threads you have (the above example will only create 1 thread per worker):
3383
- *
3384
- * ```js
3385
- * const myWorker1 = LeUtils.createWorkerThread('my-worker'); // creates a thread, you can create multiple worker threads of the same worker, to run multiple instances in parallel
3386
- * const myWorker2 = LeUtils.createWorkerThread('my-worker'); // same worker, another thread
3387
- * const {results} = await (async () =>
3388
- * {
3389
- * try
3390
- * {
3391
- * return await myWorker1.sendMessage({someData:[1, 2, 3, 4, 5]});
3392
- * }
3393
- * catch(error)
3394
- * {
3395
- * console.error('MyWorker: ', error);
3396
- * return {results:[]};
3397
- * }
3398
- * })();
3399
- * ```
3400
- *
3401
- * @param {string} name
3402
- * @returns {{worker:Worker|null, sendMessage:(data:Object,options:{timeout:number|undefined}|undefined)=>Promise<Object>}}
3403
- */
3404
- createWorkerThread:
3405
- (name) =>
3406
- {
3407
- if(!globalThis?.Worker)
3408
- {
3409
- console.warn('LeUtils.createWorkerThread: Workers are not supported in this environment, returning a dummy worker.');
3410
- return {
3411
- worker: null,
3412
- sendMessage:(data, options) => new Promise((resolve, reject) =>
3413
- {
3414
- reject('Workers are not supported in this environment');
3415
- }),
3416
- };
3417
- }
3418
-
3419
- const worker = new globalThis.Worker('/workers/' + name + '.worker.js');
3420
- let listeners = new Map();
3421
-
3422
- const addListener = (id, callback) =>
3423
- {
3424
- listeners.set(id, callback);
3425
- };
3426
-
3427
- const removeListener = (id) =>
3428
- {
3429
- listeners.delete(id);
3430
- };
3431
-
3432
- const sendMessage = (data, options) =>
3433
- {
3434
- return new Promise((resolve, reject) =>
3435
- {
3436
- const id = LeUtils.uniqueId();
3437
- addListener(id, resolve);
3438
- setTimeout(() =>
3439
- {
3440
- removeListener(id);
3441
- reject('timeout');
3442
- }, options?.timeout ?? 10000);
3443
-
3444
- worker.postMessage({
3445
- id,
3446
- ...data,
3447
- });
3448
- });
3449
- };
3450
-
3451
- worker.onerror = (error) =>
3452
- {
3453
- console.error('Worker ' + name + ':', error);
3454
- };
3455
- worker.onmessage = (message) =>
3456
- {
3457
- const data = message.data;
3458
- if(data?.id)
3459
- {
3460
- const callback = listeners.get(data.id);
3461
- if(callback)
3462
- {
3463
- removeListener(data.id);
3464
- callback(data);
3465
- }
3466
- }
3467
- };
3468
-
3469
- return {worker, sendMessage};
3470
- },
3471
-
3472
- /**
3473
- * Sends a message to the given worker. Creates a worker thread for this worker if it doesn't exist yet.
3474
- *
3475
- * See {@link LeUtils#createWorkerThread} for more info on how to use workers.
3476
- *
3477
- * @param {string} workerName
3478
- * @param {Object} data
3479
- * @param {{timeout:number|undefined}} [options]
3480
- * @returns {Promise<Object>}
3481
- */
3482
- sendWorkerMessage:
3483
- (() =>
3484
- {
3485
- const workers = new Map();
3486
- return (workerName, data, options) =>
3487
- {
3488
- if(!workers.has(workerName))
3489
- {
3490
- workers.set(workerName, LeUtils.createWorkerThread(workerName));
3491
- }
3492
- return workers.get(workerName).sendMessage(data, options);
3493
- };
3494
- })(),
3495
-
3496
- /**
3497
- * Purges the given email address, returning an empty string if it's invalid.
3498
- *
3499
- * @param {string} email
3500
- * @returns {string}
3501
- */
3502
- purgeEmail:
3503
- (email) =>
3504
- {
3505
- email = STRING(email).trim().toLowerCase().replace(/\s/g, '');
3506
- if(!email.includes('@') || !email.includes('.'))
3507
- {
3508
- return '';
3509
- }
3510
- return email;
3511
- },
3512
-
3513
- /**
3514
- * Returns true if the focus is effectively clear, meaning that the user is not typing in an input field.
3515
- *
3516
- * @returns {boolean}
3517
- */
3518
- isFocusClear:(() =>
3519
- {
3520
- const inputTypes = ['text', 'search', 'email', 'number', 'password', 'tel', 'time', 'url', 'week', 'month', 'date', 'datetime-local'];
3521
- return () => !((globalThis?.document?.activeElement?.tagName?.toLowerCase() === 'input') && inputTypes.includes(globalThis?.document?.activeElement?.getAttribute('type')?.toLowerCase() ?? ''));
3522
- })(),
3523
-
3524
- /**
3525
- * Returns the user's locale. Returns 'en-US' if it can't be determined.
3526
- *
3527
- * @returns {string}
3528
- */
3529
- getUserLocale:(() =>
3530
- {
3531
- let userLocale = null;
3532
- return () =>
3533
- {
3534
- if(userLocale === null)
3535
- {
3536
- userLocale = (() =>
3537
- {
3538
- let locales = globalThis?.navigator?.languages ?? [];
3539
- if(!IS_ARRAY(locales) || (locales.length <= 0))
3540
- {
3541
- return 'en-US';
3542
- }
3543
- locales = locales.filter(locale => ((typeof locale === 'string') && locale.includes('-') && (locale.toLowerCase() !== 'en-us')));
3544
- if(locales.length <= 0)
3545
- {
3546
- return 'en-US';
3547
- }
3548
- const localesNoEnglish = locales.filter(locale => !locale.toLowerCase().startsWith('en-'));
3549
- if(localesNoEnglish.length <= 0)
3550
- {
3551
- return locales[0];
3552
- }
3553
- return localesNoEnglish[0];
3554
- })();
3555
- }
3556
- return userLocale;
3557
- };
3558
- })(),
3559
-
3560
- /**
3561
- * Returns the user's locale date format. Always returns YYYY MM DD, with the character in between depending on the user's locale. Returns 'YYYY/MM/DD' if the user's locale can't be determined.
3562
- *
3563
- * @returns {string}
3564
- */
3565
- getUserLocaleDateFormat:(() =>
3566
- {
3567
- let userLocaleDateFormat = null;
3568
- return () =>
3569
- {
3570
- if(userLocaleDateFormat === null)
3571
- {
3572
- userLocaleDateFormat = (() =>
3573
- {
3574
- let char = '/';
3575
- if(globalThis?.Intl?.DateTimeFormat)
3576
- {
3577
- const formattedDate = new globalThis.Intl.DateTimeFormat(LeUtils.getUserLocale()).format();
3578
- if(formattedDate.includes('-'))
3579
- {
3580
- char = '-';
3581
- }
3582
- else if(formattedDate.includes('. '))
3583
- {
3584
- char = '.';
3585
- }
3586
- else if(formattedDate.includes('.'))
3587
- {
3588
- char = '.';
3589
- }
3590
- }
3591
- return 'YYYY' + char + 'MM' + char + 'DD';
3592
- })();
3593
- }
3594
- return userLocaleDateFormat;
3595
- };
3596
- })(),
3597
- };