@lowentry/utils 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LeTypes.js +121 -0
  2. package/LeUtils.js +1289 -115
  3. package/index.js +2 -2
  4. package/package.json +1 -1
package/LeUtils.js CHANGED
@@ -1,19 +1,20 @@
1
- import FastDeepEqual from 'fast-deep-equal';
2
- import {ISSET, IS_OBJECT, STRING, INT, FLOAT, FLOAT_ANY, INT_LAX} from './LeTypes.js';
1
+ import {ISSET, IS_OBJECT, STRING, INT_LAX, FLOAT_LAX, INT_LAX_ANY, FLOAT_LAX_ANY} from './LeTypes.js';
3
2
 
4
3
 
5
4
  export const LeUtils = {
6
- equals:FastDeepEqual,
7
-
8
- getCleanErrorMessage:
9
- (error) =>
10
- {
11
- const message = STRING(((typeof error === 'string') ? error : (error.message ?? JSON.stringify(error))));
12
- const messageParts = message.split('threw an error:');
13
- return messageParts[messageParts.length - 1].trim();
14
- },
15
-
16
- /** expects a version string like "1.2.3" or "1.2.3 r0" **/
5
+ /**
6
+ * Parses the given version string, and returns an object with the major, minor, and patch numbers, as well as some comparison functions.
7
+ *
8
+ * Expects a version string such as:
9
+ * - "1"
10
+ * - "1.2"
11
+ * - "1.2.3"
12
+ * - "1.2.3 anything"
13
+ * - "1.2.3-anything"
14
+ *
15
+ * @param {string|*} versionString
16
+ * @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)}}
17
+ */
17
18
  parseVersionString:
18
19
  (versionString) =>
19
20
  {
@@ -104,6 +105,15 @@ export const LeUtils = {
104
105
  return THIS;
105
106
  },
106
107
 
108
+ /**
109
+ * Returns true if the array or object contains the given value.
110
+ *
111
+ * Values are compared by casting both of them to a string.
112
+ *
113
+ * @param {array|object|Function} array
114
+ * @param {*} value
115
+ * @returns {boolean}
116
+ */
107
117
  contains:
108
118
  (array, value) =>
109
119
  {
@@ -124,6 +134,15 @@ export const LeUtils = {
124
134
  return result;
125
135
  },
126
136
 
137
+ /**
138
+ * Returns true if the array or object contains the given value.
139
+ *
140
+ * Values are compared by casting both of them to a string, and then lowercasing them.
141
+ *
142
+ * @param {array|object|Function} array
143
+ * @param {*} value
144
+ * @returns {boolean}
145
+ */
127
146
  containsCaseInsensitive:
128
147
  (array, value) =>
129
148
  {
@@ -144,6 +163,187 @@ export const LeUtils = {
144
163
  return result;
145
164
  },
146
165
 
166
+ /**
167
+ * Returns true if the array or object contains all the given values.
168
+ *
169
+ * Values are compared by casting both of them to a string.
170
+ *
171
+ * @param {array|object|Function} array
172
+ * @param {array|object|Function} values
173
+ * @returns {boolean}
174
+ */
175
+ containsAll:
176
+ (array, values) =>
177
+ {
178
+ if(!array)
179
+ {
180
+ return false;
181
+ }
182
+ let result = true;
183
+ LeUtils.each(values, function(value)
184
+ {
185
+ if(!LeUtils.contains(array, value))
186
+ {
187
+ result = false;
188
+ return false;
189
+ }
190
+ });
191
+ return result;
192
+ },
193
+
194
+ /**
195
+ * Returns true if the array or object contains all the given values.
196
+ *
197
+ * Values are compared by casting both of them to a string, and then lowercasing them.
198
+ *
199
+ * @param {array|object|Function} array
200
+ * @param {array|object|Function} values
201
+ * @returns {boolean}
202
+ */
203
+ containsAllCaseInsensitive:
204
+ (array, values) =>
205
+ {
206
+ if(!array)
207
+ {
208
+ return false;
209
+ }
210
+ let result = true;
211
+ LeUtils.each(values, function(value)
212
+ {
213
+ if(!LeUtils.containsCaseInsensitive(array, value))
214
+ {
215
+ result = false;
216
+ return false;
217
+ }
218
+ });
219
+ return result;
220
+ },
221
+
222
+ /**
223
+ * Returns true if the array or object contains any of the given values.
224
+ *
225
+ * Values are compared by casting both of them to a string.
226
+ *
227
+ * @param {array|object|Function} array
228
+ * @param {array|object|Function} values
229
+ * @returns {boolean}
230
+ */
231
+ containsAny:
232
+ (array, values) =>
233
+ {
234
+ if(!array)
235
+ {
236
+ return false;
237
+ }
238
+ let result = false;
239
+ LeUtils.each(values, function(value)
240
+ {
241
+ if(LeUtils.contains(array, value))
242
+ {
243
+ result = true;
244
+ return false;
245
+ }
246
+ });
247
+ return result;
248
+ },
249
+
250
+ /**
251
+ * Returns true if the array or object contains any of the given values.
252
+ *
253
+ * Values are compared by casting both of them to a string, and then lowercasing them.
254
+ *
255
+ * @param {array|object|Function} array
256
+ * @param {array|object|Function} values
257
+ * @returns {boolean}
258
+ */
259
+ containsAnyCaseInsensitive:
260
+ (array, values) =>
261
+ {
262
+ if(!array)
263
+ {
264
+ return false;
265
+ }
266
+ let result = false;
267
+ LeUtils.each(values, function(value)
268
+ {
269
+ if(LeUtils.containsCaseInsensitive(array, value))
270
+ {
271
+ result = true;
272
+ return false;
273
+ }
274
+ });
275
+ return result;
276
+ },
277
+
278
+ /**
279
+ * Returns true if the array or object contains none of the given values.
280
+ *
281
+ * Values are compared by casting both of them to a string.
282
+ *
283
+ * @param {array|object|Function} array
284
+ * @param {array|object|Function} values
285
+ * @returns {boolean}
286
+ */
287
+ containsNone:
288
+ (array, values) =>
289
+ {
290
+ if(!array)
291
+ {
292
+ return true;
293
+ }
294
+ let result = true;
295
+ LeUtils.each(values, function(value)
296
+ {
297
+ if(LeUtils.contains(array, value))
298
+ {
299
+ result = false;
300
+ return false;
301
+ }
302
+ });
303
+ return result;
304
+ },
305
+
306
+ /**
307
+ * Returns true if the array or object contains none of the given values.
308
+ *
309
+ * Values are compared by casting both of them to a string, and then lowercasing them.
310
+ *
311
+ * @param {array|object|Function} array
312
+ * @param {array|object|Function} values
313
+ * @returns {boolean}
314
+ */
315
+ containsNoneCaseInsensitive:
316
+ (array, values) =>
317
+ {
318
+ if(!array)
319
+ {
320
+ return true;
321
+ }
322
+ let result = true;
323
+ LeUtils.each(values, function(value)
324
+ {
325
+ if(LeUtils.containsCaseInsensitive(array, value))
326
+ {
327
+ result = false;
328
+ return false;
329
+ }
330
+ });
331
+ return result;
332
+ },
333
+
334
+ /**
335
+ * @callback LeUtils~__eachCallback
336
+ * @param {*} value
337
+ * @param {*} index
338
+ */
339
+ /**
340
+ * Loops through each element in the given array or object, and calls the callback for each element.
341
+ *
342
+ * @param {*[]|object|Function} elements
343
+ * @param {LeUtils~__eachCallback} callback
344
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
345
+ * @returns {*[]|object|Function}
346
+ */
147
347
  each:
148
348
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
149
349
  {
@@ -180,6 +380,115 @@ export const LeUtils = {
180
380
  return elements;
181
381
  },
182
382
 
383
+ /**
384
+ * Like LeUtils.each(), except that it expects an async callback.
385
+ *
386
+ * @param {*[]|object|function} elements
387
+ * @param {LeUtils~__eachCallback} asyncCallback
388
+ * @param {number} [optionalParallelCount]
389
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
390
+ * @returns {*[]|object|function}
391
+ */
392
+ eachAsync:
393
+ (() =>
394
+ {
395
+ const eachAsyncParallel = async (elements, asyncCallback, optionalParallelCount, optionalSkipHasOwnPropertyCheck) =>
396
+ {
397
+ let promises = [];
398
+ let doBreak = false;
399
+ await LeUtils.eachAsync(elements, async (element, index) =>
400
+ {
401
+ while(promises.length > optionalParallelCount)
402
+ {
403
+ let newPromises = [];
404
+ LeUtils.each(promises, (promise) =>
405
+ {
406
+ if(!promise.__lowentry_utils__promise_is_done__)
407
+ {
408
+ newPromises.push(promise);
409
+ }
410
+ });
411
+ promises = newPromises;
412
+ if(promises.length > optionalParallelCount)
413
+ {
414
+ await Promise.any(promises);
415
+ }
416
+ }
417
+
418
+ if(doBreak)
419
+ {
420
+ return false;
421
+ }
422
+
423
+ const promise = (async () =>
424
+ {
425
+ if((await asyncCallback.call(element, element, index)) === false)
426
+ {
427
+ doBreak = true;
428
+ }
429
+ promise.__lowentry_utils__promise_is_done__ = true;
430
+ })();
431
+ promises.push(promise);
432
+ }, optionalSkipHasOwnPropertyCheck);
433
+ await Promise.all(promises);
434
+ return elements;
435
+ };
436
+
437
+ return async (elements, asyncCallback, parallelCount = 1, optionalSkipHasOwnPropertyCheck = false) =>
438
+ {
439
+ if((elements !== null) && (typeof elements !== 'undefined'))
440
+ {
441
+ parallelCount = INT_LAX(parallelCount);
442
+ if(parallelCount > 1)
443
+ {
444
+ return await eachAsyncParallel(elements, asyncCallback, parallelCount, optionalSkipHasOwnPropertyCheck);
445
+ }
446
+
447
+ if(Array.isArray(elements))
448
+ {
449
+ for(let index = 0; index < elements.length; index++)
450
+ {
451
+ if((await asyncCallback.call(elements[index], elements[index], index)) === false)
452
+ {
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ else if((typeof elements === 'object') || (typeof elements === 'function'))
458
+ {
459
+ for(let index in elements)
460
+ {
461
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index))
462
+ {
463
+ if((await asyncCallback.call(elements[index], elements[index], index)) === false)
464
+ {
465
+ break;
466
+ }
467
+ }
468
+ }
469
+ }
470
+ else
471
+ {
472
+ console.warn('Executed LeUtils.eachAsync() on an invalid type: [' + (typeof elements) + ']', elements);
473
+ }
474
+ }
475
+ return elements;
476
+ };
477
+ })(),
478
+
479
+ /**
480
+ * @callback LeUtils~__filterCallback
481
+ * @param {*} value
482
+ * @param {*} index
483
+ */
484
+ /**
485
+ * Loops through the given elements, and returns a new array or object, with only the elements that didn't return false from the callback.
486
+ *
487
+ * @param {*[]|object|Function} elements
488
+ * @param {LeUtils~__filterCallback} callback
489
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
490
+ * @returns {*[]|object|Function}
491
+ */
183
492
  filter:
184
493
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
185
494
  {
@@ -220,6 +529,19 @@ export const LeUtils = {
220
529
  return elements;
221
530
  },
222
531
 
532
+ /**
533
+ * @callback LeUtils~__mapCallback
534
+ * @param {*} value
535
+ * @param {*} index
536
+ */
537
+ /**
538
+ * Loops through the given elements, and returns a new array or object, with the elements that were returned from the callback.
539
+ *
540
+ * @param {*[]|object|Function} elements
541
+ * @param {LeUtils~__mapCallback} callback
542
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
543
+ * @returns {*[]|object|Function}
544
+ */
223
545
  map:
224
546
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
225
547
  {
@@ -254,6 +576,19 @@ export const LeUtils = {
254
576
  return elements;
255
577
  },
256
578
 
579
+ /**
580
+ * @callback LeUtils~__mapToArrayCallback
581
+ * @param {*} value
582
+ * @param {*} index
583
+ */
584
+ /**
585
+ * Loops through the given elements, and returns a new array, with the elements that were returned from the callback. Always returns an array.
586
+ *
587
+ * @param {*[]|object|Function} elements
588
+ * @param {LeUtils~__mapToArrayCallback} callback
589
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
590
+ * @returns {*[]}
591
+ */
257
592
  mapToArray:
258
593
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
259
594
  {
@@ -285,6 +620,20 @@ export const LeUtils = {
285
620
  return result;
286
621
  },
287
622
 
623
+ /**
624
+ * @callback LeUtils~__mapToArraySortedCallback
625
+ * @param {*} value
626
+ * @param {*} index
627
+ */
628
+ /**
629
+ * 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.
630
+ *
631
+ * @param {*[]|object|Function} elements
632
+ * @param {LeUtils~__sortKeysComparatorCallback} comparator
633
+ * @param {LeUtils~__mapToArraySortedCallback} callback
634
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
635
+ * @returns {*[]}
636
+ */
288
637
  mapToArraySorted:
289
638
  (elements, comparator, callback, optionalSkipHasOwnPropertyCheck = false) =>
290
639
  {
@@ -297,6 +646,19 @@ export const LeUtils = {
297
646
  return result;
298
647
  },
299
648
 
649
+ /**
650
+ * @callback LeUtils~__sortKeysComparatorCallback
651
+ * @param {*} elementA
652
+ * @param {*} elementB
653
+ */
654
+ /**
655
+ * 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.
656
+ *
657
+ * @param {*[]|object|Function} elements
658
+ * @param {LeUtils~__sortKeysComparatorCallback} comparator
659
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
660
+ * @returns {*[]}
661
+ */
300
662
  sortKeys:
301
663
  (elements, comparator, optionalSkipHasOwnPropertyCheck = false) =>
302
664
  {
@@ -329,6 +691,52 @@ export const LeUtils = {
329
691
  return keys;
330
692
  },
331
693
 
694
+ /**
695
+ * Turns the given value(s) into a 1 dimensional array.
696
+ *
697
+ * Does the same thing as Array.flat(Infinity).
698
+ *
699
+ * @param {*} array
700
+ * @returns {*[]}
701
+ */
702
+ flattenArray:
703
+ (() =>
704
+ {
705
+ const flattenArrayRecursive = (result, array) =>
706
+ {
707
+ if(!Array.isArray(array))
708
+ {
709
+ result.push(array);
710
+ return;
711
+ }
712
+ array.forEach((entry) =>
713
+ {
714
+ flattenArrayRecursive(result, entry);
715
+ });
716
+ };
717
+
718
+ return (array) =>
719
+ {
720
+ if(!Array.isArray(array))
721
+ {
722
+ return [array];
723
+ }
724
+ let result = [];
725
+ array.forEach((entry) =>
726
+ {
727
+ flattenArrayRecursive(result, entry);
728
+ });
729
+ return result;
730
+ };
731
+ })(),
732
+
733
+ /**
734
+ * Compares two values. Primarily used for sorting.
735
+ *
736
+ * @param {*} a
737
+ * @param {*} b
738
+ * @returns {number}
739
+ */
332
740
  compare:
333
741
  (a, b) =>
334
742
  {
@@ -343,9 +751,23 @@ export const LeUtils = {
343
751
  return 0;
344
752
  },
345
753
 
754
+ /**
755
+ * Compares two numbers. Primarily used for sorting.
756
+ *
757
+ * @param {number} a
758
+ * @param {number} b
759
+ * @returns {number}
760
+ */
346
761
  compareNumbers:
347
762
  (a, b) => a - b,
348
763
 
764
+ /**
765
+ * Compares two numeric strings. Primarily used for sorting.
766
+ *
767
+ * @param {string|number} a
768
+ * @param {string|number} b
769
+ * @returns {number}
770
+ */
349
771
  compareNumericStrings:
350
772
  (a, b) =>
351
773
  {
@@ -358,59 +780,53 @@ export const LeUtils = {
358
780
  return (a.length < b.length) ? -1 : 1;
359
781
  },
360
782
 
783
+ /**
784
+ * Returns true if the given object is empty, false otherwise.
785
+ *
786
+ * @param {object} obj
787
+ * @param [optionalSkipHasOwnPropertyCheck]
788
+ * @returns {boolean}
789
+ */
361
790
  isEmptyObject:
362
- (obj) =>
791
+ (obj, optionalSkipHasOwnPropertyCheck = false) =>
363
792
  {
364
- // noinspection LoopStatementThatDoesntLoopJS
365
- for(let name in obj)
793
+ for(let field in obj)
366
794
  {
367
- return false;
795
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
796
+ {
797
+ return false;
798
+ }
368
799
  }
369
800
  return true;
370
801
  },
371
802
 
803
+ /**
804
+ * Returns the number of fields in the given object.
805
+ *
806
+ * @param {object} obj
807
+ * @param [optionalSkipHasOwnPropertyCheck]
808
+ * @returns {number}
809
+ */
372
810
  getObjectFieldsCount:
373
- (obj) =>
811
+ (obj, optionalSkipHasOwnPropertyCheck = false) =>
374
812
  {
375
813
  let count = 0;
376
- for(let name in obj)
814
+ for(let field in obj)
377
815
  {
378
- count++;
379
- }
380
- return count;
381
- },
382
-
383
- flattenArray:
384
- (() =>
385
- {
386
- const flattenArrayRecursive = (result, array) =>
387
- {
388
- if(!Array.isArray(array))
816
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
389
817
  {
390
- result.push(array);
391
- return;
818
+ count++;
392
819
  }
393
- array.forEach((entry) =>
394
- {
395
- flattenArrayRecursive(result, entry);
396
- });
397
- };
398
-
399
- return (array) =>
400
- {
401
- if(!Array.isArray(array))
402
- {
403
- return [array];
404
- }
405
- let result = [];
406
- array.forEach((entry) =>
407
- {
408
- flattenArrayRecursive(result, entry);
409
- });
410
- return result;
411
- };
412
- })(),
820
+ }
821
+ return count;
822
+ },
413
823
 
824
+ /**
825
+ * Returns true if the given function is a generator function (like: "function* (){}"), returns false otherwise.
826
+ *
827
+ * @param {Function} func
828
+ * @returns {boolean}
829
+ */
414
830
  isGeneratorFunction:
415
831
  (() =>
416
832
  {
@@ -426,7 +842,7 @@ export const LeUtils = {
426
842
  {
427
843
  }.constructor;
428
844
 
429
- const PossibleGeneratorFunctionNames = Array.from(new Set(['GeneratorFunction', 'AsyncFunction', 'AsyncGeneratorFunction', GeneratorFunction.name, GeneratorFunction.displayName, AsyncGeneratorFunction.name, AsyncGeneratorFunction.displayName])).filter(function(element)
845
+ const PossibleGeneratorFunctionNames = Array.from(new Set(['GeneratorFunction', 'AsyncFunction', 'AsyncGeneratorFunction', GeneratorFunction.name, GeneratorFunction.displayName, AsyncGeneratorFunction.name, AsyncGeneratorFunction.displayName])).filter((element) =>
430
846
  {
431
847
  return (element && (element !== RegularFunction.name) && (element !== RegularFunction.displayName));
432
848
  });
@@ -446,10 +862,70 @@ export const LeUtils = {
446
862
  };
447
863
  })(),
448
864
 
865
+ /**
866
+ * @callback LeUtils~__setTimeoutCallback
867
+ * @param {number} deltaTime
868
+ */
869
+ /**
870
+ * Executes the callback after the given number of milliseconds. Passes the elapsed time in seconds to the callback.
871
+ *
872
+ * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setTimeout((deltaTime)=>{}, 1000); timeoutHandler.remove();")
873
+ *
874
+ * @param {LeUtils~__setTimeoutCallback} callback ([number] deltaTime)
875
+ * @param {number} ms
876
+ * @returns {{remove:Function}}
877
+ */
878
+ setTimeout:
879
+ (callback, ms) =>
880
+ {
881
+ ms = FLOAT_LAX(ms);
882
+
883
+ let lastTime = performance.now();
884
+ let handler = setTimeout(() =>
885
+ {
886
+ const currentTime = performance.now();
887
+ try
888
+ {
889
+ callback((currentTime - lastTime) / 1000);
890
+ }
891
+ catch(e)
892
+ {
893
+ console.error(e);
894
+ }
895
+ lastTime = currentTime;
896
+ }, ms);
897
+
898
+ return {
899
+ remove:
900
+ () =>
901
+ {
902
+ if(handler !== null)
903
+ {
904
+ clearTimeout(handler);
905
+ handler = null;
906
+ }
907
+ },
908
+ };
909
+ },
910
+
911
+ /**
912
+ * @callback LeUtils~__setIntervalCallback
913
+ * @param {number} deltaTime
914
+ */
915
+ /**
916
+ * Executes the callback every given number of milliseconds. Passes the time difference in seconds between the last frame and now to it.
917
+ *
918
+ * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setInterval((deltaTime)=>{}, 1000); intervalHandler.remove();")
919
+ *
920
+ * @param {LeUtils~__setIntervalCallback} callback ([number] deltaTime)
921
+ * @param {number} [intervalMs]
922
+ * @param {boolean} [fireImmediately]
923
+ * @returns {{remove:Function}}
924
+ */
449
925
  setInterval:
450
- (callback, intervalMs, fireImmediately) =>
926
+ (callback, intervalMs = 1000, fireImmediately = false) =>
451
927
  {
452
- intervalMs = FLOAT_ANY(intervalMs, 1000);
928
+ intervalMs = FLOAT_LAX_ANY(intervalMs, 1000);
453
929
 
454
930
  if(fireImmediately)
455
931
  {
@@ -466,7 +942,7 @@ export const LeUtils = {
466
942
  let lastTime = performance.now();
467
943
  let handler = setInterval(() =>
468
944
  {
469
- let currentTime = performance.now();
945
+ const currentTime = performance.now();
470
946
  try
471
947
  {
472
948
  callback((currentTime - lastTime) / 1000);
@@ -491,34 +967,36 @@ export const LeUtils = {
491
967
  };
492
968
  },
493
969
 
494
- setAnimationFrameInterval:
495
- (callback, intervalFrames, fireImmediately) =>
970
+ /**
971
+ * @callback LeUtils~__setAnimationFrameTimeoutCallback
972
+ * @param {number} deltaTime
973
+ */
974
+ /**
975
+ * Executes the callback after the given number of frames. Passes the elapsed time in seconds to the callback.
976
+ *
977
+ * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setAnimationFrameTimeout((deltaTime){}, 5); timeoutHandler.remove();")
978
+ *
979
+ * @param {LeUtils~__setAnimationFrameTimeoutCallback} callback ([number] deltaTime)
980
+ * @param {number} [frames]
981
+ * @returns {{remove:Function}}
982
+ */
983
+ setAnimationFrameTimeout:
984
+ (callback, frames = 1) =>
496
985
  {
497
- intervalFrames = INT(intervalFrames);
498
-
499
- if(fireImmediately)
500
- {
501
- try
502
- {
503
- callback(0);
504
- }
505
- catch(e)
506
- {
507
- console.error(e);
508
- }
509
- }
986
+ frames = INT_LAX_ANY(frames, 1);
510
987
 
511
988
  let run = true;
512
989
  let requestAnimationFrameId = null;
513
990
  let lastTime = performance.now();
514
- let frames = intervalFrames;
515
991
  const tick = () =>
516
992
  {
517
993
  if(run)
518
994
  {
519
995
  if(frames <= 0)
520
996
  {
521
- let currentTime = performance.now();
997
+ run = false;
998
+ requestAnimationFrameId = null;
999
+ const currentTime = performance.now();
522
1000
  try
523
1001
  {
524
1002
  callback((currentTime - lastTime) / 1000);
@@ -528,17 +1006,13 @@ export const LeUtils = {
528
1006
  console.error(e);
529
1007
  }
530
1008
  lastTime = currentTime;
531
- frames = intervalFrames;
1009
+ return;
532
1010
  }
533
1011
  frames--;
534
-
535
- if(run)
536
- {
537
- requestAnimationFrameId = window?.requestAnimationFrame(tick);
538
- }
1012
+ requestAnimationFrameId = (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
539
1013
  }
540
1014
  };
541
- window?.requestAnimationFrame(tick);
1015
+ tick();
542
1016
 
543
1017
  return {
544
1018
  remove:
@@ -547,43 +1021,75 @@ export const LeUtils = {
547
1021
  run = false;
548
1022
  if(requestAnimationFrameId !== null)
549
1023
  {
550
- cancelAnimationFrame(requestAnimationFrameId);
1024
+ (typeof window === 'undefined') ? clearTimeout(requestAnimationFrameId) : cancelAnimationFrame(requestAnimationFrameId);
551
1025
  requestAnimationFrameId = null;
552
1026
  }
553
1027
  },
554
1028
  };
555
1029
  },
556
1030
 
557
- setAnimationFrameTimeout:
558
- (callback, frames) =>
1031
+ /**
1032
+ * @callback LeUtils~__setAnimationFrameIntervalCallback
1033
+ * @param {number} deltaTime
1034
+ */
1035
+ /**
1036
+ * Executes the callback every given number of frames. Passes the time difference in seconds between the last frame and now to it.
1037
+ *
1038
+ * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setAnimationFrameInterval((deltaTime)=>{}, 5); intervalHandler.remove();")
1039
+ *
1040
+ * @param {LeUtils~__setAnimationFrameIntervalCallback} callback ([number] deltaTime)
1041
+ * @param {number} [intervalFrames]
1042
+ * @param {boolean} [fireImmediately]
1043
+ * @returns {{remove:Function}}
1044
+ */
1045
+ setAnimationFrameInterval:
1046
+ (callback, intervalFrames = 1, fireImmediately = false) =>
559
1047
  {
560
- frames = INT(frames);
1048
+ intervalFrames = INT_LAX_ANY(intervalFrames, 1);
1049
+
1050
+ if(fireImmediately)
1051
+ {
1052
+ try
1053
+ {
1054
+ callback(0);
1055
+ }
1056
+ catch(e)
1057
+ {
1058
+ console.error(e);
1059
+ }
1060
+ }
561
1061
 
562
1062
  let run = true;
563
1063
  let requestAnimationFrameId = null;
1064
+ let lastTime = performance.now();
1065
+ let frames = intervalFrames;
564
1066
  const tick = () =>
565
1067
  {
566
1068
  if(run)
567
1069
  {
568
1070
  if(frames <= 0)
569
1071
  {
570
- run = false;
571
- requestAnimationFrameId = null;
1072
+ let currentTime = performance.now();
572
1073
  try
573
1074
  {
574
- callback();
1075
+ callback((currentTime - lastTime) / 1000);
575
1076
  }
576
1077
  catch(e)
577
1078
  {
578
1079
  console.error(e);
579
1080
  }
580
- return;
1081
+ lastTime = currentTime;
1082
+ frames = intervalFrames;
581
1083
  }
582
1084
  frames--;
583
- requestAnimationFrameId = window?.requestAnimationFrame(tick);
1085
+
1086
+ if(run)
1087
+ {
1088
+ requestAnimationFrameId = (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
1089
+ }
584
1090
  }
585
1091
  };
586
- tick();
1092
+ (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
587
1093
 
588
1094
  return {
589
1095
  remove:
@@ -592,35 +1098,45 @@ export const LeUtils = {
592
1098
  run = false;
593
1099
  if(requestAnimationFrameId !== null)
594
1100
  {
595
- cancelAnimationFrame(requestAnimationFrameId);
1101
+ (typeof window === 'undefined') ? clearTimeout(requestAnimationFrameId) : cancelAnimationFrame(requestAnimationFrameId);
596
1102
  requestAnimationFrameId = null;
597
1103
  }
598
1104
  },
599
1105
  };
600
1106
  },
601
1107
 
602
- capitalize:
603
- (string) =>
1108
+ /**
1109
+ * Returns a promise, which will be resolved after the given number of milliseconds.
1110
+ *
1111
+ * @param {number} ms
1112
+ * @returns {Promise}
1113
+ */
1114
+ promiseTimeout:
1115
+ (ms) =>
604
1116
  {
605
- string = STRING(string).trim();
606
- if(string.length <= 0)
1117
+ ms = FLOAT_LAX(ms);
1118
+ if(ms <= 0)
607
1119
  {
608
- return string;
1120
+ return new Promise(resolve => resolve());
609
1121
  }
610
- return string.charAt(0).toUpperCase() + string.slice(1);
1122
+ return new Promise(resolve => setTimeout(resolve, ms));
611
1123
  },
612
1124
 
613
- stopPropagation:
614
- (callback) =>
1125
+ /**
1126
+ * Returns a promise, which will be resolved after the given number of frames.
1127
+ *
1128
+ * @param {number} frames
1129
+ * @returns {Promise}
1130
+ */
1131
+ promiseAnimationFrameTimeout:
1132
+ (frames) =>
615
1133
  {
616
- return (event) =>
1134
+ frames = INT_LAX(frames);
1135
+ if(frames <= 0)
617
1136
  {
618
- event.stopPropagation();
619
- if(typeof callback !== 'undefined')
620
- {
621
- callback();
622
- }
623
- };
1137
+ return new Promise(resolve => resolve());
1138
+ }
1139
+ return new Promise(resolve => LeUtils.setAnimationFrameTimeout(resolve, frames));
624
1140
  },
625
1141
 
626
1142
  /**
@@ -631,13 +1147,19 @@ export const LeUtils = {
631
1147
  * - Mobile: True
632
1148
  * - Tablet: False
633
1149
  * - Desktop: False
1150
+ *
1151
+ * @returns {boolean}
634
1152
  */
635
1153
  platformIsMobile:
636
1154
  () =>
637
1155
  {
1156
+ if(typeof window === 'undefined')
1157
+ {
1158
+ return false;
1159
+ }
638
1160
  // noinspection JSDeprecatedSymbols, JSUnresolvedReference
639
1161
  /** navigator.userAgentData.mobile doesn't return the correct value on some platforms, so this is a work-around, code from: http://detectmobilebrowsers.com **/
640
- const a = STRING(window?.navigator?.userAgent || window?.navigator?.vendor || window?.opera || '');
1162
+ const a = STRING(window.navigator?.userAgent || window.navigator?.vendor || window.opera || '');
641
1163
  const b = a.substring(0, 4);
642
1164
  return !!(
643
1165
  /(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
@@ -650,24 +1172,43 @@ export const LeUtils = {
650
1172
  /**
651
1173
  * Returns true if the user has a cursor (mouse, touchpad, etc).
652
1174
  * In this context, a cursor is defined as an input device that can hover over elements without necessarily interacting with them.
1175
+ *
1176
+ * @returns {boolean}
653
1177
  */
654
1178
  platformHasCursor:
655
1179
  () =>
656
1180
  {
657
- return !LeUtils.platformIsMobile() && !window?.matchMedia('(any-hover: none)')?.matches;
1181
+ if(typeof window === 'undefined')
1182
+ {
1183
+ return true;
1184
+ }
1185
+ return !LeUtils.platformIsMobile() && !window.matchMedia('(any-hover: none)')?.matches;
658
1186
  },
659
1187
 
660
- promiseTimeout:
661
- (timeoutMs) =>
1188
+ /**
1189
+ * Returns the given string, with the first character capitalized.
1190
+ *
1191
+ * @param {String} string
1192
+ * @returns {string}
1193
+ */
1194
+ capitalize:
1195
+ (string) =>
662
1196
  {
663
- timeoutMs = FLOAT(timeoutMs);
664
- if(timeoutMs <= 0)
1197
+ string = STRING(string).trim();
1198
+ if(string.length <= 0)
665
1199
  {
666
- return new Promise(resolve => resolve());
1200
+ return string;
667
1201
  }
668
- return new Promise(resolve => setTimeout(resolve, timeoutMs));
1202
+ return string.charAt(0).toUpperCase() + string.slice(1);
669
1203
  },
670
1204
 
1205
+ /**
1206
+ * Returns true if the given string ends with any of the given characters or words.
1207
+ *
1208
+ * @param {string} string
1209
+ * @param {string|string[]} endingCharsStringOrArray
1210
+ * @returns {boolean}
1211
+ */
671
1212
  endsWithAny:
672
1213
  (string, endingCharsStringOrArray) =>
673
1214
  {
@@ -693,6 +1234,13 @@ export const LeUtils = {
693
1234
  return result;
694
1235
  },
695
1236
 
1237
+ /**
1238
+ * Returns true if the given string starts with any of the given characters or words.
1239
+ *
1240
+ * @param {string} string
1241
+ * @param {string|string[]} startingCharsStringOrArray
1242
+ * @returns {boolean}
1243
+ */
696
1244
  startsWithAny:
697
1245
  (string, startingCharsStringOrArray) =>
698
1246
  {
@@ -718,6 +1266,12 @@ export const LeUtils = {
718
1266
  return result;
719
1267
  },
720
1268
 
1269
+ /**
1270
+ * Trims the end of the given string, by removing the given characters from it.
1271
+ *
1272
+ * @param {string} string
1273
+ * @param {string|string[]} trimCharsStringOrArray
1274
+ */
721
1275
  trimEnd:
722
1276
  (string, trimCharsStringOrArray) =>
723
1277
  {
@@ -749,6 +1303,12 @@ export const LeUtils = {
749
1303
  return string;
750
1304
  },
751
1305
 
1306
+ /**
1307
+ * Trims the start of the given string, by removing the given characters from it.
1308
+ *
1309
+ * @param {string} string
1310
+ * @param {string|string[]} trimCharsStringOrArray
1311
+ */
752
1312
  trimStart:
753
1313
  (string, trimCharsStringOrArray) =>
754
1314
  {
@@ -780,10 +1340,22 @@ export const LeUtils = {
780
1340
  return string;
781
1341
  },
782
1342
 
1343
+ /**
1344
+ * Trims the start and end of the given string, by removing the given characters from it.
1345
+ *
1346
+ * @param {string} string
1347
+ * @param {string|string[]} trimCharsStringOrArray
1348
+ */
783
1349
  trim:
784
1350
  (string, trimCharsStringOrArray) => LeUtils.trimEnd(LeUtils.trimStart(string, trimCharsStringOrArray), trimCharsStringOrArray),
785
1351
 
786
- cleanupSentence:
1352
+ /**
1353
+ * Returns the given string, trims the start and end, and makes sure it ends with a valid sentence ending character (such as !?;.).
1354
+ *
1355
+ * @param {string} sentence
1356
+ * @returns {string}
1357
+ */
1358
+ purgeSentence:
787
1359
  (sentence) =>
788
1360
  {
789
1361
  sentence = LeUtils.trimEnd(STRING(sentence).trim(), '.: \r\n\t');
@@ -791,6 +1363,26 @@ export const LeUtils = {
791
1363
  return sentence;
792
1364
  },
793
1365
 
1366
+ /**
1367
+ * Attempts to obtain and return an error message from the given error, regardless of what is passed to this function.
1368
+ *
1369
+ * @param {*} error
1370
+ * @returns {string}
1371
+ */
1372
+ purgeErrorMessage:
1373
+ (error) =>
1374
+ {
1375
+ const message = STRING(((typeof error === 'string') ? error : (error.message ?? JSON.stringify(error))));
1376
+ const messageParts = message.split('threw an error:');
1377
+ return messageParts[messageParts.length - 1].trim();
1378
+ },
1379
+
1380
+ /**
1381
+ * Increases the given numeric string by 1, this allows you to increase a numeric string without a limit.
1382
+ *
1383
+ * @param {string} string
1384
+ * @returns {string}
1385
+ */
794
1386
  increaseNumericStringByOne:
795
1387
  (string) =>
796
1388
  {
@@ -832,6 +1424,11 @@ export const LeUtils = {
832
1424
  return string;
833
1425
  },
834
1426
 
1427
+ /**
1428
+ * Generates a string that is guaranteed to be unique (across the entire frontend).
1429
+ *
1430
+ * @returns {string}
1431
+ */
835
1432
  uniqueId:
836
1433
  (() =>
837
1434
  {
@@ -881,10 +1478,587 @@ export const LeUtils = {
881
1478
  };
882
1479
  })(),
883
1480
 
1481
+ /**
1482
+ * Returns a data URL of a 1x1 transparent pixel.
1483
+ *
1484
+ * @returns {string}
1485
+ */
884
1486
  getEmptyImageSrc:
885
1487
  () =>
886
1488
  {
887
1489
  // noinspection SpellCheckingInspection
888
1490
  return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
889
1491
  },
1492
+
1493
+ /**
1494
+ * Calculates and returns the percentage of the part and total ((part / total) * 100).
1495
+ *
1496
+ * @param {number|string} part
1497
+ * @param {number|string} total
1498
+ * @returns {number}
1499
+ */
1500
+ getPercentage:
1501
+ (part, total) =>
1502
+ {
1503
+ part = FLOAT_LAX(part);
1504
+ total = FLOAT_LAX(total);
1505
+ if(total <= 0)
1506
+ {
1507
+ return 100;
1508
+ }
1509
+ return Math.max(0, Math.min(100, ((part / total) * 100)));
1510
+ },
1511
+
1512
+ /**
1513
+ * Returns the pixels of the given Image object.
1514
+ *
1515
+ * @param {HTMLImageElement} image
1516
+ * @returns {Uint8ClampedArray}
1517
+ */
1518
+ getImagePixels:
1519
+ (image) =>
1520
+ {
1521
+ if(!document)
1522
+ {
1523
+ return new Uint8ClampedArray();
1524
+ }
1525
+ const canvas = document.createElement('canvas');
1526
+ document.body.appendChild(canvas);
1527
+ try
1528
+ {
1529
+ const ctx = canvas.getContext('2d');
1530
+ const width = Math.floor(image.width);
1531
+ const height = Math.floor(image.height);
1532
+ if((width <= 0) || (height <= 0))
1533
+ {
1534
+ canvas.width = 1;
1535
+ canvas.height = 1;
1536
+ }
1537
+ else
1538
+ {
1539
+ canvas.width = width;
1540
+ canvas.height = height;
1541
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1542
+ }
1543
+ return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
1544
+ }
1545
+ finally
1546
+ {
1547
+ canvas.parentNode.removeChild(canvas);
1548
+ }
1549
+ },
1550
+
1551
+ /**
1552
+ * Returns the data URL (mimetype "image/png") of a colored version of the given Image object.
1553
+ *
1554
+ * @param {HTMLImageElement} image
1555
+ * @param {string} color
1556
+ * @returns {string}
1557
+ */
1558
+ getColoredImage:
1559
+ (image, color) =>
1560
+ {
1561
+ if(!document)
1562
+ {
1563
+ return LeUtils.getEmptyImageSrc();
1564
+ }
1565
+ const canvas = document.createElement('canvas');
1566
+ document.body.appendChild(canvas);
1567
+ try
1568
+ {
1569
+ const ctx = canvas.getContext('2d');
1570
+ const width = Math.floor(image.width);
1571
+ const height = Math.floor(image.height);
1572
+ if((width <= 0) || (height <= 0))
1573
+ {
1574
+ canvas.width = 1;
1575
+ canvas.height = 1;
1576
+ }
1577
+ else
1578
+ {
1579
+ canvas.width = width;
1580
+ canvas.height = height;
1581
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1582
+ }
1583
+ ctx.globalCompositeOperation = 'source-in';
1584
+ ctx.fillStyle = color;
1585
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1586
+ return canvas.toDataURL('image/png');
1587
+ }
1588
+ finally
1589
+ {
1590
+ canvas.parentNode.removeChild(canvas);
1591
+ }
1592
+ },
1593
+
1594
+ /**
1595
+ * Returns the hex color of the given RGB(A).
1596
+ *
1597
+ * @param {number[]} rgb
1598
+ * @returns {string}
1599
+ */
1600
+ rgbToHex:
1601
+ (rgb) =>
1602
+ {
1603
+ return '#' + rgb.map((x) =>
1604
+ {
1605
+ const hex = x.toString(16);
1606
+ return ((hex.length === 1) ? '0' + hex : hex);
1607
+ }).join('');
1608
+ },
1609
+
1610
+ /**
1611
+ * Returns the Lab of the given RGB.
1612
+ *
1613
+ * @param {number[]} rgb
1614
+ * @returns {number[]}
1615
+ */
1616
+ rgbToLab:
1617
+ (rgb) =>
1618
+ {
1619
+ let r = rgb[0] / 255;
1620
+ let g = rgb[1] / 255;
1621
+ let b = rgb[2] / 255;
1622
+ r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
1623
+ g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
1624
+ b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
1625
+ let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
1626
+ let y = (r * 0.2126 + g * 0.7152 + b * 0.0722);
1627
+ let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
1628
+ x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116;
1629
+ y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116;
1630
+ z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116;
1631
+ return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
1632
+ },
1633
+
1634
+ /**
1635
+ * Returns the HSL(A) of the given RGB(A).
1636
+ *
1637
+ * @param {number[]} rgb
1638
+ * @returns {number[]}
1639
+ */
1640
+ rgbToHsl:
1641
+ (rgb) =>
1642
+ {
1643
+ const r = rgb[0] / 255;
1644
+ const g = rgb[1] / 255;
1645
+ const b = rgb[2] / 255;
1646
+
1647
+ const max = Math.max(r, g, b);
1648
+ const min = Math.min(r, g, b);
1649
+ let h, s, l = (max + min) / 2;
1650
+
1651
+ if(max === min)
1652
+ {
1653
+ h = s = 0;
1654
+ }
1655
+ else
1656
+ {
1657
+ const d = max - min;
1658
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1659
+ switch(max)
1660
+ {
1661
+ case r:
1662
+ h = (g - b) / d + (g < b ? 6 : 0);
1663
+ break;
1664
+ case g:
1665
+ h = (b - r) / d + 2;
1666
+ break;
1667
+ case b:
1668
+ h = (r - g) / d + 4;
1669
+ break;
1670
+ }
1671
+ h /= 6;
1672
+ }
1673
+
1674
+ if(rgb.length >= 4)
1675
+ {
1676
+ return [h, s, l, rgb[3] / 255];
1677
+ }
1678
+ return [h, s, l];
1679
+ },
1680
+
1681
+ /**
1682
+ * Returns the difference (calculated with DeltaE) of the Lab values of the given RGB values.
1683
+ *
1684
+ * Returns a number:
1685
+ *
1686
+ * <pre>
1687
+ * < 1.0 is not perceptible by human eyes
1688
+ * 1-2 is perceptible through close observation
1689
+ * 2-10 is perceptible at a glance
1690
+ * 11-49 is more similar than opposite
1691
+ * 100 is exactly the opposite color
1692
+ * </pre>
1693
+ *
1694
+ * @param {number[]} rgbA
1695
+ * @param {number[]} rgbB
1696
+ * @returns {number}
1697
+ */
1698
+ getDifferenceBetweenRgb:
1699
+ (rgbA, rgbB) =>
1700
+ {
1701
+ const labA = LeUtils.rgbToLab(rgbA);
1702
+ const labB = LeUtils.rgbToLab(rgbB);
1703
+ return LeUtils.getDifferenceBetweenLab(labA, labB);
1704
+ },
1705
+
1706
+ /**
1707
+ * Returns the difference (calculated with DeltaE) of the given Lab values.
1708
+ *
1709
+ * Returns a number:
1710
+ *
1711
+ * <pre>
1712
+ * < 1.0 is not perceptible by human eyes
1713
+ * 1-2 is perceptible through close observation
1714
+ * 2-10 is perceptible at a glance
1715
+ * 11-49 is more similar than opposite
1716
+ * 100 is exactly the opposite color
1717
+ * </pre>
1718
+ *
1719
+ * @param {number[]} labA
1720
+ * @param {number[]} labB
1721
+ * @returns {number}
1722
+ */
1723
+ getDifferenceBetweenLab:
1724
+ (labA, labB) =>
1725
+ {
1726
+ const deltaL = labA[0] - labB[0];
1727
+ const deltaA = labA[1] - labB[1];
1728
+ const deltaB = labA[2] - labB[2];
1729
+ const c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
1730
+ const c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
1731
+ const deltaC = c1 - c2;
1732
+ let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
1733
+ deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
1734
+ const sc = 1.0 + 0.045 * c1;
1735
+ const sh = 1.0 + 0.015 * c1;
1736
+ const deltaLKlsl = deltaL / (1.0);
1737
+ const deltaCkcsc = deltaC / (sc);
1738
+ const deltaHkhsh = deltaH / (sh);
1739
+ const i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
1740
+ return i < 0 ? 0 : Math.sqrt(i);
1741
+ },
1742
+
1743
+ /**
1744
+ * Returns the RGB(A) of the given RGB(A) values, based on the given percentage (0-100).
1745
+ * This allows you to define a gradient of colors to fade in between, rather than just having a start and an end color.
1746
+ *
1747
+ * Usage:
1748
+ *
1749
+ * <pre>
1750
+ * LeUtils.getRgbOfGradient({
1751
+ * 0: [255, 0, 0],
1752
+ * 33: [255, 255, 0],
1753
+ * 66: [0, 255, 0],
1754
+ * 100:[0, 255, 255],
1755
+ * }, 45.1234);
1756
+ * </pre>
1757
+ *
1758
+ * @param {{[percentage]: number[]}} gradient
1759
+ * @param {number} percentage
1760
+ * @returns {number[]}
1761
+ */
1762
+ getRgbOfGradient:
1763
+ (gradient, percentage) =>
1764
+ {
1765
+ percentage = Math.max(0, Math.min(100, FLOAT_LAX(percentage)));
1766
+
1767
+ let closest = null;
1768
+ LeUtils.each(gradient, (color, percent) =>
1769
+ {
1770
+ percent = INT_LAX(percent);
1771
+ if(closest === null)
1772
+ {
1773
+ closest = [percent, Math.abs(percentage - percent)];
1774
+ }
1775
+ else
1776
+ {
1777
+ const difference = Math.abs(percentage - percent);
1778
+ if(difference < closest[1])
1779
+ {
1780
+ closest = [percent, difference];
1781
+ }
1782
+ }
1783
+ });
1784
+ if(closest === null)
1785
+ {
1786
+ return null;
1787
+ }
1788
+ closest = closest[0];
1789
+
1790
+ let higher = 99999;
1791
+ let lower = -99999;
1792
+ LeUtils.each(gradient, (color, percent) =>
1793
+ {
1794
+ percent = INT_LAX(percent);
1795
+ if(percent < closest)
1796
+ {
1797
+ if(percent > lower)
1798
+ {
1799
+ lower = percent;
1800
+ }
1801
+ }
1802
+ if(percent > closest)
1803
+ {
1804
+ if(percent < higher)
1805
+ {
1806
+ higher = percent;
1807
+ }
1808
+ }
1809
+ });
1810
+ if(higher === 99999)
1811
+ {
1812
+ higher = null;
1813
+ }
1814
+ if(lower === -99999)
1815
+ {
1816
+ lower = null;
1817
+ }
1818
+
1819
+ if(((higher === null) && (lower === null)) || (higher === lower))
1820
+ {
1821
+ return gradient[closest];
1822
+ }
1823
+ else if((higher !== null) && (lower !== null))
1824
+ {
1825
+ const higherDifference = Math.abs(higher - percentage);
1826
+ const lowerDifference = Math.abs(percentage - lower);
1827
+ if(higherDifference > lowerDifference)
1828
+ {
1829
+ higher = closest;
1830
+ }
1831
+ else
1832
+ {
1833
+ lower = closest;
1834
+ }
1835
+ }
1836
+ else if(lower === null)
1837
+ {
1838
+ lower = closest;
1839
+ }
1840
+ else
1841
+ {
1842
+ higher = closest;
1843
+ }
1844
+
1845
+ if(lower > higher)
1846
+ {
1847
+ const tmp = higher;
1848
+ higher = lower;
1849
+ lower = tmp;
1850
+ }
1851
+
1852
+ const total = (higher - lower);
1853
+ const part = (percentage - lower);
1854
+ return LeUtils.getRgbBetween(gradient[lower], gradient[higher], ((part / total) * 100));
1855
+ },
1856
+
1857
+ /**
1858
+ * Returns the RGB(A) between the two given RGB(A) values, based on the given percentage (0-100).
1859
+ *
1860
+ * @param {number[]} startRgb
1861
+ * @param {number[]} endRgb
1862
+ * @param {number} percentage
1863
+ * @returns {number[]}
1864
+ */
1865
+ getRgbBetween:
1866
+ (startRgb, endRgb, percentage) =>
1867
+ {
1868
+ percentage = FLOAT_LAX(percentage);
1869
+ const partEnd = Math.max(0, Math.min(1, (percentage / 100.0)));
1870
+ const partStart = (1 - partEnd);
1871
+ const length = Math.min(startRgb.length, endRgb.length);
1872
+ let result = [];
1873
+ for(let i = 0; i < length; i++)
1874
+ {
1875
+ result.push(Math.max(0, Math.min(255, Math.round((startRgb[i] * partStart) + (endRgb[i] * partEnd)))));
1876
+ }
1877
+ return result;
1878
+ },
1879
+
1880
+ /**
1881
+ * An implementation of the btoa function, which should work in all environments.
1882
+ *
1883
+ * @param {string} string
1884
+ * @returns {string}
1885
+ */
1886
+ btoa:
1887
+ (string) =>
1888
+ {
1889
+ if(typeof btoa === 'function')
1890
+ {
1891
+ return btoa(string);
1892
+ }
1893
+ return Buffer.from(string).toString('base64');
1894
+ },
1895
+
1896
+ /**
1897
+ * An implementation of the atob function, which should work in all environments.
1898
+ *
1899
+ * @param {string} base64string
1900
+ * @returns {string}
1901
+ */
1902
+ atob:
1903
+ (base64string) =>
1904
+ {
1905
+ if(typeof atob === 'function')
1906
+ {
1907
+ return atob(base64string);
1908
+ }
1909
+ return Buffer.from(base64string, 'base64').toString();
1910
+ },
1911
+
1912
+ /**
1913
+ * Encodes a UTF-8 string into a base64 string.
1914
+ *
1915
+ * @param {string} string
1916
+ * @returns {string}
1917
+ */
1918
+ utf8ToBase64:
1919
+ (string) =>
1920
+ {
1921
+ return LeUtils.btoa(encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16))));
1922
+ },
1923
+
1924
+ /**
1925
+ * Decodes a base64 string back into a UTF-8 string.
1926
+ *
1927
+ * @param {string} base64string
1928
+ * @returns {string}
1929
+ */
1930
+ base64ToUtf8:
1931
+ (base64string) =>
1932
+ {
1933
+ return decodeURIComponent(LeUtils.atob(base64string).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
1934
+ },
1935
+
1936
+ /**
1937
+ * Converts a base64 string into a hex string.
1938
+ *
1939
+ * @param {string} base64string
1940
+ * @returns {string}
1941
+ */
1942
+ base64ToHex:
1943
+ (base64string) =>
1944
+ {
1945
+ return LeUtils.atob(base64string).split('').map((c) => ('0' + c.charCodeAt(0).toString(16)).slice(-2)).join('');
1946
+ },
1947
+
1948
+ /**
1949
+ * Converts a hex string into a base64 string.
1950
+ *
1951
+ * @param {string} hexstring
1952
+ * @returns {string}
1953
+ */
1954
+ hexToBase64:
1955
+ (hexstring) =>
1956
+ {
1957
+ return LeUtils.btoa(hexstring.match(/\w{2}/g).map((a) => String.fromCharCode(parseInt(a, 16))).join(''));
1958
+ },
1959
+
1960
+ /**
1961
+ * Converts a base64 string into bytes (Uint8Array).
1962
+ *
1963
+ * @param {string} base64string
1964
+ * @returns {Uint8Array}
1965
+ */
1966
+ base64ToBytes:
1967
+ (base64string) =>
1968
+ {
1969
+ const binary = LeUtils.atob(base64string);
1970
+ const len = binary.length;
1971
+ let data = new Uint8Array(len);
1972
+ for(let i = 0; i < len; i++)
1973
+ {
1974
+ data[i] = binary.charCodeAt(i);
1975
+ }
1976
+ return data;
1977
+ },
1978
+
1979
+ /**
1980
+ * Converts bytes into a base64 string.
1981
+ *
1982
+ * @param {ArrayLike<number>|ArrayBufferLike} arraybuffer
1983
+ * @returns {string}
1984
+ */
1985
+ bytesToBase64:
1986
+ (arraybuffer) =>
1987
+ {
1988
+ const bytes = new Uint8Array(arraybuffer);
1989
+ const len = bytes.byteLength;
1990
+ let binary = '';
1991
+ for(let i = 0; i < len; i++)
1992
+ {
1993
+ binary += String.fromCharCode(bytes[i]);
1994
+ }
1995
+ return LeUtils.btoa(binary);
1996
+ },
1997
+
1998
+ /**
1999
+ * Loads the value from the browser, returns undefined if the value doesn't exist.
2000
+ *
2001
+ * @param {string} id
2002
+ * @returns {*}
2003
+ */
2004
+ localStorageGet:
2005
+ (id) =>
2006
+ {
2007
+ if(typeof window === 'undefined')
2008
+ {
2009
+ return;
2010
+ }
2011
+ let result = window.localStorage.getItem('LeUtils_' + id);
2012
+ if(typeof result !== 'string')
2013
+ {
2014
+ return;
2015
+ }
2016
+ try
2017
+ {
2018
+ result = JSON.parse(result);
2019
+ if(typeof result['-'] !== 'undefined')
2020
+ {
2021
+ return result['-'];
2022
+ }
2023
+ }
2024
+ catch(e)
2025
+ {
2026
+ }
2027
+ },
2028
+
2029
+ /**
2030
+ * Saves the given data in the browser.
2031
+ *
2032
+ * @param {string} id
2033
+ * @param {*} data
2034
+ */
2035
+ localStorageSet:
2036
+ (id, data) =>
2037
+ {
2038
+ if(typeof window === 'undefined')
2039
+ {
2040
+ return;
2041
+ }
2042
+ if(typeof data === 'undefined')
2043
+ {
2044
+ window.localStorage.removeItem('LeUtils_' + id);
2045
+ return;
2046
+ }
2047
+ window.localStorage.setItem('LeUtils_' + id, JSON.stringify({'-':data}));
2048
+ },
2049
+
2050
+ /**
2051
+ * Removes the data from the browser.
2052
+ *
2053
+ * @param {string} id
2054
+ */
2055
+ localStorageRemove:
2056
+ (id) =>
2057
+ {
2058
+ if(typeof window === 'undefined')
2059
+ {
2060
+ return;
2061
+ }
2062
+ window.localStorage.removeItem('LeUtils_' + id);
2063
+ },
890
2064
  };