@lowentry/utils 0.1.1 → 0.2.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.
Files changed (4) hide show
  1. package/LeTypes.js +121 -0
  2. package/LeUtils.js +1829 -132
  3. package/index.js +2 -2
  4. package/package.json +1 -1
package/LeUtils.js CHANGED
@@ -1,19 +1,52 @@
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
- export const LeUtils = {
6
- equals:FastDeepEqual,
7
-
8
- getCleanErrorMessage:
9
- (error) =>
4
+ /**
5
+ * @param {LeUtils~TransactionalValue} transactionalValue
6
+ */
7
+ const checkTransactionalValue = (transactionalValue) =>
8
+ {
9
+ if(!LeUtils.isTransactionalValueValid(transactionalValue))
10
+ {
11
+ console.error('The given value is not a valid TransactionalValue:');
12
+ console.error(transactionalValue);
13
+ throw new Error('The given value is not a valid TransactionalValue');
14
+ }
15
+ };
16
+
17
+ /**
18
+ * @param {LeUtils~TransactionalValue} transactionalValue
19
+ * @param {string} changeId
20
+ * @returns {{index:number, value:*}|null}
21
+ */
22
+ const findTransactionalValueChange = (transactionalValue, changeId) =>
23
+ {
24
+ for(let i = 0; i < transactionalValue.changes.length; i++)
25
+ {
26
+ const change = transactionalValue.changes[i];
27
+ if(change.id === changeId)
10
28
  {
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" **/
29
+ return {index:i, value:change.value};
30
+ }
31
+ }
32
+ return null;
33
+ };
34
+
35
+
36
+ export const LeUtils = {
37
+ /**
38
+ * Parses the given version string, and returns an object with the major, minor, and patch numbers, as well as some comparison functions.
39
+ *
40
+ * Expects a version string such as:
41
+ * - "1"
42
+ * - "1.2"
43
+ * - "1.2.3"
44
+ * - "1.2.3 anything"
45
+ * - "1.2.3-anything"
46
+ *
47
+ * @param {string|*} versionString
48
+ * @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)}}
49
+ */
17
50
  parseVersionString:
18
51
  (versionString) =>
19
52
  {
@@ -104,6 +137,15 @@ export const LeUtils = {
104
137
  return THIS;
105
138
  },
106
139
 
140
+ /**
141
+ * Returns true if the array or object contains the given value.
142
+ *
143
+ * Values are compared by casting both of them to a string.
144
+ *
145
+ * @param {array|object|Function} array
146
+ * @param {*} value
147
+ * @returns {boolean}
148
+ */
107
149
  contains:
108
150
  (array, value) =>
109
151
  {
@@ -124,6 +166,15 @@ export const LeUtils = {
124
166
  return result;
125
167
  },
126
168
 
169
+ /**
170
+ * Returns true if the array or object contains the given value.
171
+ *
172
+ * Values are compared by casting both of them to a string, and then lowercasing them.
173
+ *
174
+ * @param {array|object|Function} array
175
+ * @param {*} value
176
+ * @returns {boolean}
177
+ */
127
178
  containsCaseInsensitive:
128
179
  (array, value) =>
129
180
  {
@@ -144,6 +195,187 @@ export const LeUtils = {
144
195
  return result;
145
196
  },
146
197
 
198
+ /**
199
+ * Returns true if the array or object contains all the given values.
200
+ *
201
+ * Values are compared by casting both of them to a string.
202
+ *
203
+ * @param {array|object|Function} array
204
+ * @param {array|object|Function} values
205
+ * @returns {boolean}
206
+ */
207
+ containsAll:
208
+ (array, values) =>
209
+ {
210
+ if(!array)
211
+ {
212
+ return false;
213
+ }
214
+ let result = true;
215
+ LeUtils.each(values, function(value)
216
+ {
217
+ if(!LeUtils.contains(array, value))
218
+ {
219
+ result = false;
220
+ return false;
221
+ }
222
+ });
223
+ return result;
224
+ },
225
+
226
+ /**
227
+ * Returns true if the array or object contains all the given values.
228
+ *
229
+ * Values are compared by casting both of them to a string, and then lowercasing them.
230
+ *
231
+ * @param {array|object|Function} array
232
+ * @param {array|object|Function} values
233
+ * @returns {boolean}
234
+ */
235
+ containsAllCaseInsensitive:
236
+ (array, values) =>
237
+ {
238
+ if(!array)
239
+ {
240
+ return false;
241
+ }
242
+ let result = true;
243
+ LeUtils.each(values, function(value)
244
+ {
245
+ if(!LeUtils.containsCaseInsensitive(array, value))
246
+ {
247
+ result = false;
248
+ return false;
249
+ }
250
+ });
251
+ return result;
252
+ },
253
+
254
+ /**
255
+ * Returns true if the array or object contains any of the given values.
256
+ *
257
+ * Values are compared by casting both of them to a string.
258
+ *
259
+ * @param {array|object|Function} array
260
+ * @param {array|object|Function} values
261
+ * @returns {boolean}
262
+ */
263
+ containsAny:
264
+ (array, values) =>
265
+ {
266
+ if(!array)
267
+ {
268
+ return false;
269
+ }
270
+ let result = false;
271
+ LeUtils.each(values, function(value)
272
+ {
273
+ if(LeUtils.contains(array, value))
274
+ {
275
+ result = true;
276
+ return false;
277
+ }
278
+ });
279
+ return result;
280
+ },
281
+
282
+ /**
283
+ * Returns true if the array or object contains any of the given values.
284
+ *
285
+ * Values are compared by casting both of them to a string, and then lowercasing them.
286
+ *
287
+ * @param {array|object|Function} array
288
+ * @param {array|object|Function} values
289
+ * @returns {boolean}
290
+ */
291
+ containsAnyCaseInsensitive:
292
+ (array, values) =>
293
+ {
294
+ if(!array)
295
+ {
296
+ return false;
297
+ }
298
+ let result = false;
299
+ LeUtils.each(values, function(value)
300
+ {
301
+ if(LeUtils.containsCaseInsensitive(array, value))
302
+ {
303
+ result = true;
304
+ return false;
305
+ }
306
+ });
307
+ return result;
308
+ },
309
+
310
+ /**
311
+ * Returns true if the array or object contains none of the given values.
312
+ *
313
+ * Values are compared by casting both of them to a string.
314
+ *
315
+ * @param {array|object|Function} array
316
+ * @param {array|object|Function} values
317
+ * @returns {boolean}
318
+ */
319
+ containsNone:
320
+ (array, values) =>
321
+ {
322
+ if(!array)
323
+ {
324
+ return true;
325
+ }
326
+ let result = true;
327
+ LeUtils.each(values, function(value)
328
+ {
329
+ if(LeUtils.contains(array, value))
330
+ {
331
+ result = false;
332
+ return false;
333
+ }
334
+ });
335
+ return result;
336
+ },
337
+
338
+ /**
339
+ * Returns true if the array or object contains none of the given values.
340
+ *
341
+ * Values are compared by casting both of them to a string, and then lowercasing them.
342
+ *
343
+ * @param {array|object|Function} array
344
+ * @param {array|object|Function} values
345
+ * @returns {boolean}
346
+ */
347
+ containsNoneCaseInsensitive:
348
+ (array, values) =>
349
+ {
350
+ if(!array)
351
+ {
352
+ return true;
353
+ }
354
+ let result = true;
355
+ LeUtils.each(values, function(value)
356
+ {
357
+ if(LeUtils.containsCaseInsensitive(array, value))
358
+ {
359
+ result = false;
360
+ return false;
361
+ }
362
+ });
363
+ return result;
364
+ },
365
+
366
+ /**
367
+ * @callback LeUtils~__eachCallback
368
+ * @param {*} value
369
+ * @param {*} index
370
+ */
371
+ /**
372
+ * Loops through each element in the given array or object, and calls the callback for each element.
373
+ *
374
+ * @param {*[]|object|Function} elements
375
+ * @param {LeUtils~__eachCallback} callback
376
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
377
+ * @returns {*[]|object|Function}
378
+ */
147
379
  each:
148
380
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
149
381
  {
@@ -180,6 +412,115 @@ export const LeUtils = {
180
412
  return elements;
181
413
  },
182
414
 
415
+ /**
416
+ * Like LeUtils.each(), except that it expects an async callback.
417
+ *
418
+ * @param {*[]|object|function} elements
419
+ * @param {LeUtils~__eachCallback} asyncCallback
420
+ * @param {number} [optionalParallelCount]
421
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
422
+ * @returns {*[]|object|function}
423
+ */
424
+ eachAsync:
425
+ (() =>
426
+ {
427
+ const eachAsyncParallel = async (elements, asyncCallback, optionalParallelCount, optionalSkipHasOwnPropertyCheck) =>
428
+ {
429
+ let promises = [];
430
+ let doBreak = false;
431
+ await LeUtils.eachAsync(elements, async (element, index) =>
432
+ {
433
+ while(promises.length > optionalParallelCount)
434
+ {
435
+ let newPromises = [];
436
+ LeUtils.each(promises, (promise) =>
437
+ {
438
+ if(!promise.__lowentry_utils__promise_is_done__)
439
+ {
440
+ newPromises.push(promise);
441
+ }
442
+ });
443
+ promises = newPromises;
444
+ if(promises.length > optionalParallelCount)
445
+ {
446
+ await Promise.any(promises);
447
+ }
448
+ }
449
+
450
+ if(doBreak)
451
+ {
452
+ return false;
453
+ }
454
+
455
+ const promise = (async () =>
456
+ {
457
+ if((await asyncCallback.call(element, element, index)) === false)
458
+ {
459
+ doBreak = true;
460
+ }
461
+ promise.__lowentry_utils__promise_is_done__ = true;
462
+ })();
463
+ promises.push(promise);
464
+ }, optionalSkipHasOwnPropertyCheck);
465
+ await Promise.all(promises);
466
+ return elements;
467
+ };
468
+
469
+ return async (elements, asyncCallback, parallelCount = 1, optionalSkipHasOwnPropertyCheck = false) =>
470
+ {
471
+ if((elements !== null) && (typeof elements !== 'undefined'))
472
+ {
473
+ parallelCount = INT_LAX(parallelCount);
474
+ if(parallelCount > 1)
475
+ {
476
+ return await eachAsyncParallel(elements, asyncCallback, parallelCount, optionalSkipHasOwnPropertyCheck);
477
+ }
478
+
479
+ if(Array.isArray(elements))
480
+ {
481
+ for(let index = 0; index < elements.length; index++)
482
+ {
483
+ if((await asyncCallback.call(elements[index], elements[index], index)) === false)
484
+ {
485
+ break;
486
+ }
487
+ }
488
+ }
489
+ else if((typeof elements === 'object') || (typeof elements === 'function'))
490
+ {
491
+ for(let index in elements)
492
+ {
493
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index))
494
+ {
495
+ if((await asyncCallback.call(elements[index], elements[index], index)) === false)
496
+ {
497
+ break;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ else
503
+ {
504
+ console.warn('Executed LeUtils.eachAsync() on an invalid type: [' + (typeof elements) + ']', elements);
505
+ }
506
+ }
507
+ return elements;
508
+ };
509
+ })(),
510
+
511
+ /**
512
+ * @callback LeUtils~__filterCallback
513
+ * @param {*} value
514
+ * @param {*} index
515
+ */
516
+ /**
517
+ * Loops through the given elements, and returns a new array or object, with only the elements that didn't return false from the callback.
518
+ *
519
+ * @param {*[]|object|Function} elements
520
+ * @param {LeUtils~__filterCallback} callback
521
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
522
+ * @returns {*[]|object|Function}
523
+ */
183
524
  filter:
184
525
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
185
526
  {
@@ -220,6 +561,19 @@ export const LeUtils = {
220
561
  return elements;
221
562
  },
222
563
 
564
+ /**
565
+ * @callback LeUtils~__mapCallback
566
+ * @param {*} value
567
+ * @param {*} index
568
+ */
569
+ /**
570
+ * Loops through the given elements, and returns a new array or object, with the elements that were returned from the callback.
571
+ *
572
+ * @param {*[]|object|Function} elements
573
+ * @param {LeUtils~__mapCallback} callback
574
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
575
+ * @returns {*[]|object|Function}
576
+ */
223
577
  map:
224
578
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
225
579
  {
@@ -254,6 +608,19 @@ export const LeUtils = {
254
608
  return elements;
255
609
  },
256
610
 
611
+ /**
612
+ * @callback LeUtils~__mapToArrayCallback
613
+ * @param {*} value
614
+ * @param {*} index
615
+ */
616
+ /**
617
+ * Loops through the given elements, and returns a new array, with the elements that were returned from the callback. Always returns an array.
618
+ *
619
+ * @param {*[]|object|Function} elements
620
+ * @param {LeUtils~__mapToArrayCallback} callback
621
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
622
+ * @returns {*[]}
623
+ */
257
624
  mapToArray:
258
625
  (elements, callback, optionalSkipHasOwnPropertyCheck = false) =>
259
626
  {
@@ -285,6 +652,20 @@ export const LeUtils = {
285
652
  return result;
286
653
  },
287
654
 
655
+ /**
656
+ * @callback LeUtils~__mapToArraySortedCallback
657
+ * @param {*} value
658
+ * @param {*} index
659
+ */
660
+ /**
661
+ * 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.
662
+ *
663
+ * @param {*[]|object|Function} elements
664
+ * @param {LeUtils~__sortKeysComparatorCallback} comparator
665
+ * @param {LeUtils~__mapToArraySortedCallback} callback
666
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
667
+ * @returns {*[]}
668
+ */
288
669
  mapToArraySorted:
289
670
  (elements, comparator, callback, optionalSkipHasOwnPropertyCheck = false) =>
290
671
  {
@@ -297,6 +678,19 @@ export const LeUtils = {
297
678
  return result;
298
679
  },
299
680
 
681
+ /**
682
+ * @callback LeUtils~__sortKeysComparatorCallback
683
+ * @param {*} elementA
684
+ * @param {*} elementB
685
+ */
686
+ /**
687
+ * 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.
688
+ *
689
+ * @param {*[]|object|Function} elements
690
+ * @param {LeUtils~__sortKeysComparatorCallback} comparator
691
+ * @param {boolean} [optionalSkipHasOwnPropertyCheck]
692
+ * @returns {*[]}
693
+ */
300
694
  sortKeys:
301
695
  (elements, comparator, optionalSkipHasOwnPropertyCheck = false) =>
302
696
  {
@@ -329,57 +723,14 @@ export const LeUtils = {
329
723
  return keys;
330
724
  },
331
725
 
332
- compare:
333
- (a, b) =>
334
- {
335
- if(a < b)
336
- {
337
- return -1;
338
- }
339
- if(a > b)
340
- {
341
- return 1;
342
- }
343
- return 0;
344
- },
345
-
346
- compareNumbers:
347
- (a, b) => a - b,
348
-
349
- compareNumericStrings:
350
- (a, b) =>
351
- {
352
- a = STRING(a).trim();
353
- b = STRING(b).trim();
354
- if(a.length === b.length)
355
- {
356
- return (a < b) ? -1 : ((a > b) ? 1 : 0);
357
- }
358
- return (a.length < b.length) ? -1 : 1;
359
- },
360
-
361
- isEmptyObject:
362
- (obj) =>
363
- {
364
- // noinspection LoopStatementThatDoesntLoopJS
365
- for(let name in obj)
366
- {
367
- return false;
368
- }
369
- return true;
370
- },
371
-
372
- getObjectFieldsCount:
373
- (obj) =>
374
- {
375
- let count = 0;
376
- for(let name in obj)
377
- {
378
- count++;
379
- }
380
- return count;
381
- },
382
-
726
+ /**
727
+ * Turns the given value(s) into a 1 dimensional array.
728
+ *
729
+ * Does the same thing as Array.flat(Infinity).
730
+ *
731
+ * @param {*} array
732
+ * @returns {*[]}
733
+ */
383
734
  flattenArray:
384
735
  (() =>
385
736
  {
@@ -411,11 +762,97 @@ export const LeUtils = {
411
762
  };
412
763
  })(),
413
764
 
414
- isGeneratorFunction:
415
- (() =>
416
- {
417
- const GeneratorFunction = function* ()
418
- {
765
+ /**
766
+ * Compares two values. Primarily used for sorting.
767
+ *
768
+ * @param {*} a
769
+ * @param {*} b
770
+ * @returns {number}
771
+ */
772
+ compare:
773
+ (a, b) => (a < b) ? -1 : ((a > b) ? 1 : 0),
774
+
775
+ /**
776
+ * Compares two numbers. Primarily used for sorting.
777
+ *
778
+ * @param {number} a
779
+ * @param {number} b
780
+ * @returns {number}
781
+ */
782
+ compareNumbers:
783
+ (a, b) => a - b,
784
+
785
+ /**
786
+ * Compares two numeric strings. Primarily used for sorting.
787
+ *
788
+ * @param {string|number} a
789
+ * @param {string|number} b
790
+ * @returns {number}
791
+ */
792
+ compareNumericStrings:
793
+ (a, b) =>
794
+ {
795
+ a = STRING(a).trim();
796
+ b = STRING(b).trim();
797
+ if(a.length === b.length)
798
+ {
799
+ return (a < b) ? -1 : ((a > b) ? 1 : 0);
800
+ }
801
+ return (a.length < b.length) ? -1 : 1;
802
+ },
803
+
804
+ /**
805
+ * Returns true if the given object is empty, false otherwise.
806
+ *
807
+ * @param {object} obj
808
+ * @param [optionalSkipHasOwnPropertyCheck]
809
+ * @returns {boolean}
810
+ */
811
+ isEmptyObject:
812
+ (obj, optionalSkipHasOwnPropertyCheck = false) =>
813
+ {
814
+ for(let field in obj)
815
+ {
816
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
817
+ {
818
+ return false;
819
+ }
820
+ }
821
+ return true;
822
+ },
823
+
824
+ /**
825
+ * Returns the number of fields in the given object.
826
+ *
827
+ * @param {object} obj
828
+ * @param [optionalSkipHasOwnPropertyCheck]
829
+ * @returns {number}
830
+ */
831
+ getObjectFieldsCount:
832
+ (obj, optionalSkipHasOwnPropertyCheck = false) =>
833
+ {
834
+ let count = 0;
835
+ for(let field in obj)
836
+ {
837
+ if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field))
838
+ {
839
+ count++;
840
+ }
841
+ }
842
+ return count;
843
+ },
844
+
845
+ /**
846
+ * Returns true if the given function is a generator function (like: "function* (){}"), returns false otherwise.
847
+ *
848
+ * @param {Function} func
849
+ * @returns {boolean}
850
+ */
851
+ isGeneratorFunction:
852
+ (() =>
853
+ {
854
+ const GeneratorFunction = function* ()
855
+ {
419
856
  }.constructor;
420
857
 
421
858
  const AsyncGeneratorFunction = async function* ()
@@ -426,7 +863,7 @@ export const LeUtils = {
426
863
  {
427
864
  }.constructor;
428
865
 
429
- const PossibleGeneratorFunctionNames = Array.from(new Set(['GeneratorFunction', 'AsyncFunction', 'AsyncGeneratorFunction', GeneratorFunction.name, GeneratorFunction.displayName, AsyncGeneratorFunction.name, AsyncGeneratorFunction.displayName])).filter(function(element)
866
+ const PossibleGeneratorFunctionNames = Array.from(new Set(['GeneratorFunction', 'AsyncFunction', 'AsyncGeneratorFunction', GeneratorFunction.name, GeneratorFunction.displayName, AsyncGeneratorFunction.name, AsyncGeneratorFunction.displayName])).filter((element) =>
430
867
  {
431
868
  return (element && (element !== RegularFunction.name) && (element !== RegularFunction.displayName));
432
869
  });
@@ -446,10 +883,70 @@ export const LeUtils = {
446
883
  };
447
884
  })(),
448
885
 
886
+ /**
887
+ * @callback LeUtils~__setTimeoutCallback
888
+ * @param {number} deltaTime
889
+ */
890
+ /**
891
+ * Executes the callback after the given number of milliseconds. Passes the elapsed time in seconds to the callback.
892
+ *
893
+ * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setTimeout((deltaTime)=>{}, 1000); timeoutHandler.remove();")
894
+ *
895
+ * @param {LeUtils~__setTimeoutCallback} callback ([number] deltaTime)
896
+ * @param {number} ms
897
+ * @returns {{remove:Function}}
898
+ */
899
+ setTimeout:
900
+ (callback, ms) =>
901
+ {
902
+ ms = FLOAT_LAX(ms);
903
+
904
+ let lastTime = performance.now();
905
+ let handler = setTimeout(() =>
906
+ {
907
+ const currentTime = performance.now();
908
+ try
909
+ {
910
+ callback((currentTime - lastTime) / 1000);
911
+ }
912
+ catch(e)
913
+ {
914
+ console.error(e);
915
+ }
916
+ lastTime = currentTime;
917
+ }, ms);
918
+
919
+ return {
920
+ remove:
921
+ () =>
922
+ {
923
+ if(handler !== null)
924
+ {
925
+ clearTimeout(handler);
926
+ handler = null;
927
+ }
928
+ },
929
+ };
930
+ },
931
+
932
+ /**
933
+ * @callback LeUtils~__setIntervalCallback
934
+ * @param {number} deltaTime
935
+ */
936
+ /**
937
+ * Executes the callback every given number of milliseconds. Passes the time difference in seconds between the last frame and now to it.
938
+ *
939
+ * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setInterval((deltaTime)=>{}, 1000); intervalHandler.remove();")
940
+ *
941
+ * @param {LeUtils~__setIntervalCallback} callback ([number] deltaTime)
942
+ * @param {number} [intervalMs]
943
+ * @param {boolean} [fireImmediately]
944
+ * @returns {{remove:Function}}
945
+ */
449
946
  setInterval:
450
- (callback, intervalMs, fireImmediately) =>
947
+ (callback, intervalMs = 1000, fireImmediately = false) =>
451
948
  {
452
- intervalMs = FLOAT_ANY(intervalMs, 1000);
949
+ intervalMs = FLOAT_LAX_ANY(intervalMs, 1000);
453
950
 
454
951
  if(fireImmediately)
455
952
  {
@@ -466,7 +963,7 @@ export const LeUtils = {
466
963
  let lastTime = performance.now();
467
964
  let handler = setInterval(() =>
468
965
  {
469
- let currentTime = performance.now();
966
+ const currentTime = performance.now();
470
967
  try
471
968
  {
472
969
  callback((currentTime - lastTime) / 1000);
@@ -491,34 +988,36 @@ export const LeUtils = {
491
988
  };
492
989
  },
493
990
 
494
- setAnimationFrameInterval:
495
- (callback, intervalFrames, fireImmediately) =>
991
+ /**
992
+ * @callback LeUtils~__setAnimationFrameTimeoutCallback
993
+ * @param {number} deltaTime
994
+ */
995
+ /**
996
+ * Executes the callback after the given number of frames. Passes the elapsed time in seconds to the callback.
997
+ *
998
+ * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = LeUtils.setAnimationFrameTimeout((deltaTime){}, 5); timeoutHandler.remove();")
999
+ *
1000
+ * @param {LeUtils~__setAnimationFrameTimeoutCallback} callback ([number] deltaTime)
1001
+ * @param {number} [frames]
1002
+ * @returns {{remove:Function}}
1003
+ */
1004
+ setAnimationFrameTimeout:
1005
+ (callback, frames = 1) =>
496
1006
  {
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
- }
1007
+ frames = INT_LAX_ANY(frames, 1);
510
1008
 
511
1009
  let run = true;
512
1010
  let requestAnimationFrameId = null;
513
1011
  let lastTime = performance.now();
514
- let frames = intervalFrames;
515
1012
  const tick = () =>
516
1013
  {
517
1014
  if(run)
518
1015
  {
519
1016
  if(frames <= 0)
520
1017
  {
521
- let currentTime = performance.now();
1018
+ run = false;
1019
+ requestAnimationFrameId = null;
1020
+ const currentTime = performance.now();
522
1021
  try
523
1022
  {
524
1023
  callback((currentTime - lastTime) / 1000);
@@ -528,17 +1027,13 @@ export const LeUtils = {
528
1027
  console.error(e);
529
1028
  }
530
1029
  lastTime = currentTime;
531
- frames = intervalFrames;
1030
+ return;
532
1031
  }
533
1032
  frames--;
534
-
535
- if(run)
536
- {
537
- requestAnimationFrameId = window?.requestAnimationFrame(tick);
538
- }
1033
+ requestAnimationFrameId = (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
539
1034
  }
540
1035
  };
541
- window?.requestAnimationFrame(tick);
1036
+ tick();
542
1037
 
543
1038
  return {
544
1039
  remove:
@@ -547,43 +1042,75 @@ export const LeUtils = {
547
1042
  run = false;
548
1043
  if(requestAnimationFrameId !== null)
549
1044
  {
550
- cancelAnimationFrame(requestAnimationFrameId);
1045
+ (typeof window === 'undefined') ? clearTimeout(requestAnimationFrameId) : cancelAnimationFrame(requestAnimationFrameId);
551
1046
  requestAnimationFrameId = null;
552
1047
  }
553
1048
  },
554
1049
  };
555
1050
  },
556
1051
 
557
- setAnimationFrameTimeout:
558
- (callback, frames) =>
1052
+ /**
1053
+ * @callback LeUtils~__setAnimationFrameIntervalCallback
1054
+ * @param {number} deltaTime
1055
+ */
1056
+ /**
1057
+ * Executes the callback every given number of frames. Passes the time difference in seconds between the last frame and now to it.
1058
+ *
1059
+ * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = LeUtils.setAnimationFrameInterval((deltaTime)=>{}, 5); intervalHandler.remove();")
1060
+ *
1061
+ * @param {LeUtils~__setAnimationFrameIntervalCallback} callback ([number] deltaTime)
1062
+ * @param {number} [intervalFrames]
1063
+ * @param {boolean} [fireImmediately]
1064
+ * @returns {{remove:Function}}
1065
+ */
1066
+ setAnimationFrameInterval:
1067
+ (callback, intervalFrames = 1, fireImmediately = false) =>
559
1068
  {
560
- frames = INT(frames);
1069
+ intervalFrames = INT_LAX_ANY(intervalFrames, 1);
1070
+
1071
+ if(fireImmediately)
1072
+ {
1073
+ try
1074
+ {
1075
+ callback(0);
1076
+ }
1077
+ catch(e)
1078
+ {
1079
+ console.error(e);
1080
+ }
1081
+ }
561
1082
 
562
1083
  let run = true;
563
1084
  let requestAnimationFrameId = null;
1085
+ let lastTime = performance.now();
1086
+ let frames = intervalFrames;
564
1087
  const tick = () =>
565
1088
  {
566
1089
  if(run)
567
1090
  {
568
1091
  if(frames <= 0)
569
1092
  {
570
- run = false;
571
- requestAnimationFrameId = null;
1093
+ let currentTime = performance.now();
572
1094
  try
573
1095
  {
574
- callback();
1096
+ callback((currentTime - lastTime) / 1000);
575
1097
  }
576
1098
  catch(e)
577
1099
  {
578
1100
  console.error(e);
579
1101
  }
580
- return;
1102
+ lastTime = currentTime;
1103
+ frames = intervalFrames;
581
1104
  }
582
1105
  frames--;
583
- requestAnimationFrameId = window?.requestAnimationFrame(tick);
1106
+
1107
+ if(run)
1108
+ {
1109
+ requestAnimationFrameId = (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
1110
+ }
584
1111
  }
585
1112
  };
586
- tick();
1113
+ (typeof window === 'undefined') ? setTimeout(tick, 1000 / 60) : requestAnimationFrame(tick);
587
1114
 
588
1115
  return {
589
1116
  remove:
@@ -592,35 +1119,45 @@ export const LeUtils = {
592
1119
  run = false;
593
1120
  if(requestAnimationFrameId !== null)
594
1121
  {
595
- cancelAnimationFrame(requestAnimationFrameId);
1122
+ (typeof window === 'undefined') ? clearTimeout(requestAnimationFrameId) : cancelAnimationFrame(requestAnimationFrameId);
596
1123
  requestAnimationFrameId = null;
597
1124
  }
598
1125
  },
599
1126
  };
600
1127
  },
601
1128
 
602
- capitalize:
603
- (string) =>
1129
+ /**
1130
+ * Returns a promise, which will be resolved after the given number of milliseconds.
1131
+ *
1132
+ * @param {number} ms
1133
+ * @returns {Promise}
1134
+ */
1135
+ promiseTimeout:
1136
+ (ms) =>
604
1137
  {
605
- string = STRING(string).trim();
606
- if(string.length <= 0)
1138
+ ms = FLOAT_LAX(ms);
1139
+ if(ms <= 0)
607
1140
  {
608
- return string;
1141
+ return new Promise(resolve => resolve());
609
1142
  }
610
- return string.charAt(0).toUpperCase() + string.slice(1);
1143
+ return new Promise(resolve => setTimeout(resolve, ms));
611
1144
  },
612
1145
 
613
- stopPropagation:
614
- (callback) =>
1146
+ /**
1147
+ * Returns a promise, which will be resolved after the given number of frames.
1148
+ *
1149
+ * @param {number} frames
1150
+ * @returns {Promise}
1151
+ */
1152
+ promiseAnimationFrameTimeout:
1153
+ (frames) =>
615
1154
  {
616
- return (event) =>
1155
+ frames = INT_LAX(frames);
1156
+ if(frames <= 0)
617
1157
  {
618
- event.stopPropagation();
619
- if(typeof callback !== 'undefined')
620
- {
621
- callback();
622
- }
623
- };
1158
+ return new Promise(resolve => resolve());
1159
+ }
1160
+ return new Promise(resolve => LeUtils.setAnimationFrameTimeout(resolve, frames));
624
1161
  },
625
1162
 
626
1163
  /**
@@ -631,13 +1168,19 @@ export const LeUtils = {
631
1168
  * - Mobile: True
632
1169
  * - Tablet: False
633
1170
  * - Desktop: False
1171
+ *
1172
+ * @returns {boolean}
634
1173
  */
635
1174
  platformIsMobile:
636
1175
  () =>
637
1176
  {
1177
+ if(typeof window === 'undefined')
1178
+ {
1179
+ return false;
1180
+ }
638
1181
  // noinspection JSDeprecatedSymbols, JSUnresolvedReference
639
1182
  /** 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 || '');
1183
+ const a = STRING(window.navigator?.userAgent || window.navigator?.vendor || window.opera || '');
641
1184
  const b = a.substring(0, 4);
642
1185
  return !!(
643
1186
  /(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 +1193,43 @@ export const LeUtils = {
650
1193
  /**
651
1194
  * Returns true if the user has a cursor (mouse, touchpad, etc).
652
1195
  * In this context, a cursor is defined as an input device that can hover over elements without necessarily interacting with them.
1196
+ *
1197
+ * @returns {boolean}
653
1198
  */
654
1199
  platformHasCursor:
655
1200
  () =>
656
1201
  {
657
- return !LeUtils.platformIsMobile() && !window?.matchMedia('(any-hover: none)')?.matches;
1202
+ if(typeof window === 'undefined')
1203
+ {
1204
+ return true;
1205
+ }
1206
+ return !LeUtils.platformIsMobile() && !window.matchMedia('(any-hover: none)')?.matches;
658
1207
  },
659
1208
 
660
- promiseTimeout:
661
- (timeoutMs) =>
1209
+ /**
1210
+ * Returns the given string, with the first character capitalized.
1211
+ *
1212
+ * @param {String} string
1213
+ * @returns {string}
1214
+ */
1215
+ capitalize:
1216
+ (string) =>
662
1217
  {
663
- timeoutMs = FLOAT(timeoutMs);
664
- if(timeoutMs <= 0)
1218
+ string = STRING(string).trim();
1219
+ if(string.length <= 0)
665
1220
  {
666
- return new Promise(resolve => resolve());
1221
+ return string;
667
1222
  }
668
- return new Promise(resolve => setTimeout(resolve, timeoutMs));
1223
+ return string.charAt(0).toUpperCase() + string.slice(1);
669
1224
  },
670
1225
 
1226
+ /**
1227
+ * Returns true if the given string ends with any of the given characters or words.
1228
+ *
1229
+ * @param {string} string
1230
+ * @param {string|string[]} endingCharsStringOrArray
1231
+ * @returns {boolean}
1232
+ */
671
1233
  endsWithAny:
672
1234
  (string, endingCharsStringOrArray) =>
673
1235
  {
@@ -693,6 +1255,13 @@ export const LeUtils = {
693
1255
  return result;
694
1256
  },
695
1257
 
1258
+ /**
1259
+ * Returns true if the given string starts with any of the given characters or words.
1260
+ *
1261
+ * @param {string} string
1262
+ * @param {string|string[]} startingCharsStringOrArray
1263
+ * @returns {boolean}
1264
+ */
696
1265
  startsWithAny:
697
1266
  (string, startingCharsStringOrArray) =>
698
1267
  {
@@ -718,6 +1287,12 @@ export const LeUtils = {
718
1287
  return result;
719
1288
  },
720
1289
 
1290
+ /**
1291
+ * Trims the end of the given string, by removing the given characters from it.
1292
+ *
1293
+ * @param {string} string
1294
+ * @param {string|string[]} trimCharsStringOrArray
1295
+ */
721
1296
  trimEnd:
722
1297
  (string, trimCharsStringOrArray) =>
723
1298
  {
@@ -749,6 +1324,12 @@ export const LeUtils = {
749
1324
  return string;
750
1325
  },
751
1326
 
1327
+ /**
1328
+ * Trims the start of the given string, by removing the given characters from it.
1329
+ *
1330
+ * @param {string} string
1331
+ * @param {string|string[]} trimCharsStringOrArray
1332
+ */
752
1333
  trimStart:
753
1334
  (string, trimCharsStringOrArray) =>
754
1335
  {
@@ -780,10 +1361,22 @@ export const LeUtils = {
780
1361
  return string;
781
1362
  },
782
1363
 
1364
+ /**
1365
+ * Trims the start and end of the given string, by removing the given characters from it.
1366
+ *
1367
+ * @param {string} string
1368
+ * @param {string|string[]} trimCharsStringOrArray
1369
+ */
783
1370
  trim:
784
1371
  (string, trimCharsStringOrArray) => LeUtils.trimEnd(LeUtils.trimStart(string, trimCharsStringOrArray), trimCharsStringOrArray),
785
1372
 
786
- cleanupSentence:
1373
+ /**
1374
+ * Returns the given string, trims the start and end, and makes sure it ends with a valid sentence ending character (such as !?;.).
1375
+ *
1376
+ * @param {string} sentence
1377
+ * @returns {string}
1378
+ */
1379
+ purgeSentence:
787
1380
  (sentence) =>
788
1381
  {
789
1382
  sentence = LeUtils.trimEnd(STRING(sentence).trim(), '.: \r\n\t');
@@ -791,6 +1384,60 @@ export const LeUtils = {
791
1384
  return sentence;
792
1385
  },
793
1386
 
1387
+ /**
1388
+ * Attempts to obtain and return an error message from the given error, regardless of what is passed to this function.
1389
+ *
1390
+ * @param {*} error
1391
+ * @returns {string}
1392
+ */
1393
+ purgeErrorMessage:
1394
+ (error) =>
1395
+ {
1396
+ const message = STRING(((typeof error === 'string') ? error : (error.message ?? JSON.stringify(error))));
1397
+ const messageParts = message.split('threw an error:');
1398
+ return messageParts[messageParts.length - 1].trim();
1399
+ },
1400
+
1401
+ /**
1402
+ * Generates all permutations of the given names.
1403
+ *
1404
+ * For example, if you pass "foo" and "bar", it will return:
1405
+ * - foobar
1406
+ * - fooBar
1407
+ * - FooBar
1408
+ * - foo-bar
1409
+ * - foo_bar
1410
+ *
1411
+ * @param {string} names
1412
+ * @returns {string[]}
1413
+ */
1414
+ generateNamePermutations:
1415
+ (...names) =>
1416
+ {
1417
+ names = LeUtils.flattenArray(names)
1418
+ .map(name => STRING(name).trim().toLowerCase())
1419
+ .filter(name => (name.length > 0));
1420
+ let results = [];
1421
+ if(names.length > 0)
1422
+ {
1423
+ results.push(names.join('')); //foobar
1424
+ results.push(names.map(LeUtils.capitalize).join('')); //FooBar
1425
+ }
1426
+ if(names.length > 1)
1427
+ {
1428
+ results.push([names[0]].concat(names.slice(1).map(LeUtils.capitalize)).join('')); //fooBar
1429
+ results.push(names.join('-')); //foo-bar
1430
+ results.push(names.join('_')); //foo_bar
1431
+ }
1432
+ return results;
1433
+ },
1434
+
1435
+ /**
1436
+ * Increases the given numeric string by 1, this allows you to increase a numeric string without a limit.
1437
+ *
1438
+ * @param {string} string
1439
+ * @returns {string}
1440
+ */
794
1441
  increaseNumericStringByOne:
795
1442
  (string) =>
796
1443
  {
@@ -832,6 +1479,11 @@ export const LeUtils = {
832
1479
  return string;
833
1480
  },
834
1481
 
1482
+ /**
1483
+ * Generates a string that is guaranteed to be unique (across the entire frontend).
1484
+ *
1485
+ * @returns {string}
1486
+ */
835
1487
  uniqueId:
836
1488
  (() =>
837
1489
  {
@@ -881,10 +1533,1055 @@ export const LeUtils = {
881
1533
  };
882
1534
  })(),
883
1535
 
1536
+ /**
1537
+ * Returns a data URL of a 1x1 transparent pixel.
1538
+ *
1539
+ * @returns {string}
1540
+ */
884
1541
  getEmptyImageSrc:
885
1542
  () =>
886
1543
  {
887
1544
  // noinspection SpellCheckingInspection
888
1545
  return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
889
1546
  },
1547
+
1548
+ /**
1549
+ * Calculates and returns the percentage of the part and total ((part / total) * 100).
1550
+ *
1551
+ * @param {number|string} part
1552
+ * @param {number|string} total
1553
+ * @returns {number}
1554
+ */
1555
+ getPercentage:
1556
+ (part, total) =>
1557
+ {
1558
+ part = FLOAT_LAX(part);
1559
+ total = FLOAT_LAX(total);
1560
+ if(total <= 0)
1561
+ {
1562
+ return 100;
1563
+ }
1564
+ return Math.max(0, Math.min(100, ((part / total) * 100)));
1565
+ },
1566
+
1567
+ /**
1568
+ * Returns the pixels of the given Image object.
1569
+ *
1570
+ * @param {HTMLImageElement} image
1571
+ * @returns {Uint8ClampedArray}
1572
+ */
1573
+ getImagePixels:
1574
+ (image) =>
1575
+ {
1576
+ if(!document)
1577
+ {
1578
+ return new Uint8ClampedArray();
1579
+ }
1580
+ const canvas = document.createElement('canvas');
1581
+ document.body.appendChild(canvas);
1582
+ try
1583
+ {
1584
+ const ctx = canvas.getContext('2d');
1585
+ const width = Math.floor(image.width);
1586
+ const height = Math.floor(image.height);
1587
+ if((width <= 0) || (height <= 0))
1588
+ {
1589
+ canvas.width = 1;
1590
+ canvas.height = 1;
1591
+ }
1592
+ else
1593
+ {
1594
+ canvas.width = width;
1595
+ canvas.height = height;
1596
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1597
+ }
1598
+ return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
1599
+ }
1600
+ finally
1601
+ {
1602
+ canvas.parentNode.removeChild(canvas);
1603
+ }
1604
+ },
1605
+
1606
+ /**
1607
+ * Returns the data URL (mimetype "image/png") of a colored version of the given Image object.
1608
+ *
1609
+ * @param {HTMLImageElement} image
1610
+ * @param {string} color
1611
+ * @returns {string}
1612
+ */
1613
+ getColoredImage:
1614
+ (image, color) =>
1615
+ {
1616
+ if(!document)
1617
+ {
1618
+ return LeUtils.getEmptyImageSrc();
1619
+ }
1620
+ const canvas = document.createElement('canvas');
1621
+ document.body.appendChild(canvas);
1622
+ try
1623
+ {
1624
+ const ctx = canvas.getContext('2d');
1625
+ const width = Math.floor(image.width);
1626
+ const height = Math.floor(image.height);
1627
+ if((width <= 0) || (height <= 0))
1628
+ {
1629
+ canvas.width = 1;
1630
+ canvas.height = 1;
1631
+ }
1632
+ else
1633
+ {
1634
+ canvas.width = width;
1635
+ canvas.height = height;
1636
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1637
+ }
1638
+ ctx.globalCompositeOperation = 'source-in';
1639
+ ctx.fillStyle = color;
1640
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1641
+ return canvas.toDataURL('image/png');
1642
+ }
1643
+ finally
1644
+ {
1645
+ canvas.parentNode.removeChild(canvas);
1646
+ }
1647
+ },
1648
+
1649
+ /**
1650
+ * Returns the hex color of the given RGB(A).
1651
+ *
1652
+ * @param {number[]} rgb
1653
+ * @returns {string}
1654
+ */
1655
+ rgbToHex:
1656
+ (rgb) =>
1657
+ {
1658
+ return '#' + rgb.map((x) =>
1659
+ {
1660
+ const hex = x.toString(16);
1661
+ return ((hex.length === 1) ? '0' + hex : hex);
1662
+ }).join('');
1663
+ },
1664
+
1665
+ /**
1666
+ * Returns the RGB(A) of the given hex color.
1667
+ *
1668
+ * @param {string} hexstring
1669
+ * @returns {number[]}
1670
+ */
1671
+ hexToRgb:
1672
+ (hexstring) =>
1673
+ {
1674
+ hexstring = hexstring.replace(/[^0-9A-F]/gi, '');
1675
+ const hasAlpha = ((hexstring.length === 4) || (hexstring.length === 8));
1676
+ while(hexstring.length < 6)
1677
+ {
1678
+ hexstring = hexstring.replace(/(.)/g, '$1$1');
1679
+ }
1680
+ const result = hexstring.match(/\w{2}/g).map((a) => parseInt(a, 16));
1681
+ return [
1682
+ result[0],
1683
+ result[1],
1684
+ result[2],
1685
+ ...(hasAlpha ? [result[3]] : []),
1686
+ ];
1687
+ },
1688
+
1689
+ /**
1690
+ * Returns the HSL(A) of the given RGB(A).
1691
+ *
1692
+ * @param {number[]} rgb
1693
+ * @returns {number[]}
1694
+ */
1695
+ rgbToHsl:
1696
+ (rgb) =>
1697
+ {
1698
+ const r = rgb[0] / 255;
1699
+ const g = rgb[1] / 255;
1700
+ const b = rgb[2] / 255;
1701
+ const max = Math.max(r, g, b);
1702
+ const min = Math.min(r, g, b);
1703
+ let h, s, l = (max + min) / 2;
1704
+ if(max === min)
1705
+ {
1706
+ h = s = 0;
1707
+ }
1708
+ else
1709
+ {
1710
+ const d = max - min;
1711
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1712
+ switch(max)
1713
+ {
1714
+ case r:
1715
+ h = (g - b) / d + (g < b ? 6 : 0);
1716
+ break;
1717
+ case g:
1718
+ h = (b - r) / d + 2;
1719
+ break;
1720
+ case b:
1721
+ h = (r - g) / d + 4;
1722
+ break;
1723
+ }
1724
+ h /= 6;
1725
+ }
1726
+ return [h, s, l, ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
1727
+ },
1728
+
1729
+ /**
1730
+ * Returns the RGB(A) of the given HSL(A).
1731
+ *
1732
+ * @param {number[]} hsl
1733
+ * @returns {number[]}
1734
+ */
1735
+ hslToRgb:
1736
+ (() =>
1737
+ {
1738
+ const hue2rgb = (p, q, t) =>
1739
+ {
1740
+ if(t < 0)
1741
+ {
1742
+ t += 1;
1743
+ }
1744
+ if(t > 1)
1745
+ {
1746
+ t -= 1;
1747
+ }
1748
+ if(t < 1 / 6)
1749
+ {
1750
+ return p + (q - p) * 6 * t;
1751
+ }
1752
+ if(t < 1 / 2)
1753
+ {
1754
+ return q;
1755
+ }
1756
+ if(t < 2 / 3)
1757
+ {
1758
+ return p + (q - p) * (2 / 3 - t) * 6;
1759
+ }
1760
+ return p;
1761
+ };
1762
+ return (hsl) =>
1763
+ {
1764
+ const h = hsl[0];
1765
+ const s = hsl[1];
1766
+ const l = hsl[2];
1767
+ let r, g, b;
1768
+ if(s === 0)
1769
+ {
1770
+ r = g = b = l;
1771
+ }
1772
+ else
1773
+ {
1774
+ const q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s));
1775
+ const p = (2 * l) - q;
1776
+ r = hue2rgb(p, q, h + (1 / 3));
1777
+ g = hue2rgb(p, q, h);
1778
+ b = hue2rgb(p, q, h - (1 / 3));
1779
+ }
1780
+ return [r * 255, g * 255, b * 255, ...((hsl.length >= 4) ? [hsl[3] * 255] : [])].map((c) => Math.max(0, Math.min(255, Math.round(c))));
1781
+ };
1782
+ })(),
1783
+
1784
+ /**
1785
+ * Returns the LAB(A) of the given RGB(A).
1786
+ *
1787
+ * @param {number[]} rgb
1788
+ * @returns {number[]}
1789
+ */
1790
+ rgbToLab:
1791
+ (rgb) =>
1792
+ {
1793
+ let r = rgb[0] / 255;
1794
+ let g = rgb[1] / 255;
1795
+ let b = rgb[2] / 255;
1796
+ r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
1797
+ g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
1798
+ b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
1799
+ let x = ((r * 0.4124) + (g * 0.3576) + (b * 0.1805)) / 0.95047;
1800
+ let y = ((r * 0.2126) + (g * 0.7152) + (b * 0.0722));
1801
+ let z = ((r * 0.0193) + (g * 0.1192) + (b * 0.9505)) / 1.08883;
1802
+ x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);
1803
+ y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);
1804
+ z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);
1805
+ return [(116 * y) - 16, 500 * (x - y), 200 * (y - z), ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
1806
+ },
1807
+
1808
+ /**
1809
+ * Returns the difference (calculated with DeltaE) of the LAB values of the given RGB values.
1810
+ *
1811
+ * Returns a number:
1812
+ *
1813
+ * <pre>
1814
+ * < 1.0 is not perceptible by human eyes
1815
+ * 1-2 is perceptible through close observation
1816
+ * 2-10 is perceptible at a glance
1817
+ * 11-49 is more similar than opposite
1818
+ * 100 is exactly the opposite color
1819
+ * </pre>
1820
+ *
1821
+ * @param {number[]} rgbA
1822
+ * @param {number[]} rgbB
1823
+ * @returns {number}
1824
+ */
1825
+ getDifferenceBetweenRgb:
1826
+ (rgbA, rgbB) =>
1827
+ {
1828
+ const labA = LeUtils.rgbToLab(rgbA);
1829
+ const labB = LeUtils.rgbToLab(rgbB);
1830
+ return LeUtils.getDifferenceBetweenLab(labA, labB);
1831
+ },
1832
+
1833
+ /**
1834
+ * Returns the difference (calculated with DeltaE) of the given LAB values. Ignores the Alpha channel.
1835
+ *
1836
+ * Returns a number:
1837
+ *
1838
+ * <pre>
1839
+ * < 1.0 is not perceptible by human eyes
1840
+ * 1-2 is perceptible through close observation
1841
+ * 2-10 is perceptible at a glance
1842
+ * 11-49 is more similar than opposite
1843
+ * 100 is exactly the opposite color
1844
+ * </pre>
1845
+ *
1846
+ * @param {number[]} labA
1847
+ * @param {number[]} labB
1848
+ * @returns {number}
1849
+ */
1850
+ getDifferenceBetweenLab:
1851
+ (labA, labB) =>
1852
+ {
1853
+ const deltaL = labA[0] - labB[0];
1854
+ const deltaA = labA[1] - labB[1];
1855
+ const deltaB = labA[2] - labB[2];
1856
+ const c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
1857
+ const c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
1858
+ const deltaC = c1 - c2;
1859
+ let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
1860
+ deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
1861
+ const sc = 1.0 + 0.045 * c1;
1862
+ const sh = 1.0 + 0.015 * c1;
1863
+ const deltaLKlsl = deltaL / (1.0);
1864
+ const deltaCkcsc = deltaC / (sc);
1865
+ const deltaHkhsh = deltaH / (sh);
1866
+ const i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
1867
+ return (i < 0) ? 0 : Math.sqrt(i);
1868
+ },
1869
+
1870
+ /**
1871
+ * Returns the RGB(A) of the given RGB(A) values, based on the given percentage (0-100).
1872
+ * This allows you to define a gradient of colors to fade in between, rather than just having a start and an end color.
1873
+ *
1874
+ * Usage:
1875
+ *
1876
+ * <pre>
1877
+ * LeUtils.getRgbOfGradient({
1878
+ * 0: [255, 0, 0],
1879
+ * 33: [255, 255, 0],
1880
+ * 66: [0, 255, 0],
1881
+ * 100:[0, 255, 255],
1882
+ * }, 45.1234);
1883
+ * </pre>
1884
+ *
1885
+ * @param {{[percentage]: number[]}} gradient
1886
+ * @param {number} percentage
1887
+ * @returns {number[]}
1888
+ */
1889
+ getRgbOfGradient:
1890
+ (gradient, percentage) =>
1891
+ {
1892
+ percentage = Math.max(0, Math.min(100, FLOAT_LAX(percentage)));
1893
+
1894
+ let closest = null;
1895
+ LeUtils.each(gradient, (color, percent) =>
1896
+ {
1897
+ percent = INT_LAX(percent);
1898
+ if(closest === null)
1899
+ {
1900
+ closest = [percent, Math.abs(percentage - percent)];
1901
+ }
1902
+ else
1903
+ {
1904
+ const difference = Math.abs(percentage - percent);
1905
+ if(difference < closest[1])
1906
+ {
1907
+ closest = [percent, difference];
1908
+ }
1909
+ }
1910
+ });
1911
+ if(closest === null)
1912
+ {
1913
+ return null;
1914
+ }
1915
+ closest = closest[0];
1916
+
1917
+ let higher = 99999;
1918
+ let lower = -99999;
1919
+ LeUtils.each(gradient, (color, percent) =>
1920
+ {
1921
+ percent = INT_LAX(percent);
1922
+ if(percent < closest)
1923
+ {
1924
+ if(percent > lower)
1925
+ {
1926
+ lower = percent;
1927
+ }
1928
+ }
1929
+ if(percent > closest)
1930
+ {
1931
+ if(percent < higher)
1932
+ {
1933
+ higher = percent;
1934
+ }
1935
+ }
1936
+ });
1937
+ if(higher === 99999)
1938
+ {
1939
+ higher = null;
1940
+ }
1941
+ if(lower === -99999)
1942
+ {
1943
+ lower = null;
1944
+ }
1945
+
1946
+ if(((higher === null) && (lower === null)) || (higher === lower))
1947
+ {
1948
+ return gradient[closest];
1949
+ }
1950
+ else if((higher !== null) && (lower !== null))
1951
+ {
1952
+ const higherDifference = Math.abs(higher - percentage);
1953
+ const lowerDifference = Math.abs(percentage - lower);
1954
+ if(higherDifference > lowerDifference)
1955
+ {
1956
+ higher = closest;
1957
+ }
1958
+ else
1959
+ {
1960
+ lower = closest;
1961
+ }
1962
+ }
1963
+ else if(lower === null)
1964
+ {
1965
+ lower = closest;
1966
+ }
1967
+ else
1968
+ {
1969
+ higher = closest;
1970
+ }
1971
+
1972
+ if(lower > higher)
1973
+ {
1974
+ const tmp = higher;
1975
+ higher = lower;
1976
+ lower = tmp;
1977
+ }
1978
+
1979
+ const total = (higher - lower);
1980
+ const part = (percentage - lower);
1981
+ return LeUtils.getRgbBetween(gradient[lower], gradient[higher], ((part / total) * 100));
1982
+ },
1983
+
1984
+ /**
1985
+ * Returns the RGB(A) between the two given RGB(A) values, based on the given percentage (0-100).
1986
+ *
1987
+ * @param {number[]} startRgb
1988
+ * @param {number[]} endRgb
1989
+ * @param {number} percentage
1990
+ * @returns {number[]}
1991
+ */
1992
+ getRgbBetween:
1993
+ (startRgb, endRgb, percentage) =>
1994
+ {
1995
+ percentage = FLOAT_LAX(percentage);
1996
+ const partEnd = Math.max(0, Math.min(1, (percentage / 100.0)));
1997
+ const partStart = (1 - partEnd);
1998
+ const length = Math.min(startRgb.length, endRgb.length);
1999
+ let result = [];
2000
+ for(let i = 0; i < length; i++)
2001
+ {
2002
+ result.push(Math.max(0, Math.min(255, Math.round((startRgb[i] * partStart) + (endRgb[i] * partEnd)))));
2003
+ }
2004
+ return result;
2005
+ },
2006
+
2007
+ /**
2008
+ * An implementation of the btoa function, which should work in all environments.
2009
+ *
2010
+ * @param {string} string
2011
+ * @returns {string}
2012
+ */
2013
+ btoa:
2014
+ (string) =>
2015
+ {
2016
+ if(typeof btoa === 'function')
2017
+ {
2018
+ return btoa(string);
2019
+ }
2020
+ return Buffer.from(string).toString('base64');
2021
+ },
2022
+
2023
+ /**
2024
+ * An implementation of the atob function, which should work in all environments.
2025
+ *
2026
+ * @param {string} base64string
2027
+ * @returns {string}
2028
+ */
2029
+ atob:
2030
+ (base64string) =>
2031
+ {
2032
+ if(typeof atob === 'function')
2033
+ {
2034
+ return atob(base64string);
2035
+ }
2036
+ return Buffer.from(base64string, 'base64').toString();
2037
+ },
2038
+
2039
+ /**
2040
+ * Encodes a UTF-8 string into a base64 string.
2041
+ *
2042
+ * @param {string} string
2043
+ * @returns {string}
2044
+ */
2045
+ utf8ToBase64:
2046
+ (string) =>
2047
+ {
2048
+ return LeUtils.btoa(encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16))));
2049
+ },
2050
+
2051
+ /**
2052
+ * Decodes a base64 string back into a UTF-8 string.
2053
+ *
2054
+ * @param {string} base64string
2055
+ * @returns {string}
2056
+ */
2057
+ base64ToUtf8:
2058
+ (base64string) =>
2059
+ {
2060
+ return decodeURIComponent(LeUtils.atob(base64string.trim()).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
2061
+ },
2062
+
2063
+ /**
2064
+ * Converts a base64 string into a hex string.
2065
+ *
2066
+ * @param {string} base64string
2067
+ * @returns {string}
2068
+ */
2069
+ base64ToHex:
2070
+ (base64string) =>
2071
+ {
2072
+ return LeUtils.atob(base64string.trim()).split('').map((c) => ('0' + c.charCodeAt(0).toString(16)).slice(-2)).join('');
2073
+ },
2074
+
2075
+ /**
2076
+ * Converts a hex string into a base64 string.
2077
+ *
2078
+ * @param {string} hexstring
2079
+ * @returns {string}
2080
+ */
2081
+ hexToBase64:
2082
+ (hexstring) =>
2083
+ {
2084
+ return LeUtils.btoa(hexstring.replace(/[^0-9A-F]/gi, '').match(/\w{2}/g).map((a) => String.fromCharCode(parseInt(a, 16))).join(''));
2085
+ },
2086
+
2087
+ /**
2088
+ * Converts a base64 string into bytes (Uint8Array).
2089
+ *
2090
+ * @param {string} base64string
2091
+ * @returns {Uint8Array}
2092
+ */
2093
+ base64ToBytes:
2094
+ (base64string) =>
2095
+ {
2096
+ const binary = LeUtils.atob(base64string.trim());
2097
+ const len = binary.length;
2098
+ let data = new Uint8Array(len);
2099
+ for(let i = 0; i < len; i++)
2100
+ {
2101
+ data[i] = binary.charCodeAt(i);
2102
+ }
2103
+ return data;
2104
+ },
2105
+
2106
+ /**
2107
+ * Converts bytes into a base64 string.
2108
+ *
2109
+ * @param {ArrayLike<number>|ArrayBufferLike} arraybuffer
2110
+ * @returns {string}
2111
+ */
2112
+ bytesToBase64:
2113
+ (arraybuffer) =>
2114
+ {
2115
+ const bytes = new Uint8Array(arraybuffer);
2116
+ const len = bytes.byteLength;
2117
+ let binary = '';
2118
+ for(let i = 0; i < len; i++)
2119
+ {
2120
+ binary += String.fromCharCode(bytes[i]);
2121
+ }
2122
+ return LeUtils.btoa(binary);
2123
+ },
2124
+
2125
+ /**
2126
+ * Downloads the given base64 string as a file.
2127
+ *
2128
+ * @param {string} base64string
2129
+ * @param {string} [fileName]
2130
+ * @param {string} [mimeType]
2131
+ */
2132
+ downloadFile:
2133
+ (base64string, fileName, mimeType) =>
2134
+ {
2135
+ const link = document.createElement('a');
2136
+ link.setAttribute('download', (typeof fileName === 'string') ? fileName : 'file');
2137
+ link.href = 'data:' + mimeType + ';base64,' + base64string;
2138
+ link.setAttribute('target', '_blank');
2139
+ link.click();
2140
+ },
2141
+
2142
+ /**
2143
+ * Loads the value from the browser, returns undefined if the value doesn't exist.
2144
+ *
2145
+ * @param {string} id
2146
+ * @returns {*}
2147
+ */
2148
+ localStorageGet:
2149
+ (id) =>
2150
+ {
2151
+ if(typeof window === 'undefined')
2152
+ {
2153
+ return;
2154
+ }
2155
+ let result = window.localStorage.getItem('LeUtils_' + id);
2156
+ if(typeof result !== 'string')
2157
+ {
2158
+ return;
2159
+ }
2160
+ try
2161
+ {
2162
+ result = JSON.parse(result);
2163
+ if(typeof result['-'] !== 'undefined')
2164
+ {
2165
+ return result['-'];
2166
+ }
2167
+ }
2168
+ catch(e)
2169
+ {
2170
+ }
2171
+ },
2172
+
2173
+ /**
2174
+ * Saves the given data in the browser.
2175
+ *
2176
+ * @param {string} id
2177
+ * @param {*} data
2178
+ */
2179
+ localStorageSet:
2180
+ (id, data) =>
2181
+ {
2182
+ if(typeof window === 'undefined')
2183
+ {
2184
+ return;
2185
+ }
2186
+ if(typeof data === 'undefined')
2187
+ {
2188
+ window.localStorage.removeItem('LeUtils_' + id);
2189
+ return;
2190
+ }
2191
+ window.localStorage.setItem('LeUtils_' + id, JSON.stringify({'-':data}));
2192
+ },
2193
+
2194
+ /**
2195
+ * Removes the data from the browser.
2196
+ *
2197
+ * @param {string} id
2198
+ */
2199
+ localStorageRemove:
2200
+ (id) =>
2201
+ {
2202
+ if(typeof window === 'undefined')
2203
+ {
2204
+ return;
2205
+ }
2206
+ window.localStorage.removeItem('LeUtils_' + id);
2207
+ },
2208
+
2209
+ /**
2210
+ * Creates and returns a new TreeSet.
2211
+ * A TreeSet is a set of elements, sorted by a comparator.
2212
+ * Binary search is used to find elements, which makes it very fast to find elements.
2213
+ *
2214
+ * The comparator is also used to determine if two values are equal to each other.
2215
+ * This way, you can have values that aren't the same be treated as if they are. This can be used to deal with issues such as floating point errors for example.
2216
+ *
2217
+ * @param {*[]} elements
2218
+ * @param {Function} comparator
2219
+ * @returns {{getElements: (function(): *[]), getComparator: (function(): Function), size: (function(): number), isEmpty: (function(): boolean), contains: (function(*): boolean), first: (function(): *|undefined), last: (function(): *|undefined), pollFirst: (function(): *|undefined), pollLast: (function(): *|undefined), add: function(*), addAll: function(*[]|object), getEqualValue: (function(*): (*)), getEqualValueOrAdd: (function(*): (*))}}
2220
+ */
2221
+ createTreeSet:
2222
+ (elements, comparator) =>
2223
+ {
2224
+ comparator = comparator || LeUtils.compare;
2225
+ elements = elements || [];
2226
+ elements.sort(comparator);
2227
+
2228
+ /**
2229
+ * Performs a binary search on the elements, and returns the result.
2230
+ *
2231
+ * @param {*} value
2232
+ * @returns {{found: boolean, index: number, value: *|undefined}}
2233
+ */
2234
+ const binarySearch = (value) =>
2235
+ {
2236
+ let low = 0;
2237
+ let high = elements.length - 1;
2238
+ while(low <= high)
2239
+ {
2240
+ const mid = Math.floor((low + high) / 2);
2241
+ const midValue = elements[mid];
2242
+ const cmp = comparator(midValue, value);
2243
+ if(cmp < 0)
2244
+ {
2245
+ low = mid + 1;
2246
+ }
2247
+ else if(cmp > 0)
2248
+ {
2249
+ high = mid - 1;
2250
+ }
2251
+ else
2252
+ {
2253
+ return {found:true, index:mid, value:midValue};
2254
+ }
2255
+ }
2256
+ return {found:false, index:low, value:undefined};
2257
+ };
2258
+
2259
+ const treeSet = {
2260
+ /**
2261
+ * Returns the elements of the set.
2262
+ *
2263
+ * @returns {*[]}
2264
+ */
2265
+ getElements:
2266
+ () => elements,
2267
+
2268
+ /**
2269
+ * Returns the comparator of the set.
2270
+ *
2271
+ * @returns {Function}
2272
+ */
2273
+ getComparator:
2274
+ () => comparator,
2275
+
2276
+ /**
2277
+ * Returns the size of the set.
2278
+ *
2279
+ * @returns {number}
2280
+ */
2281
+ size:
2282
+ () => elements.length,
2283
+
2284
+ /**
2285
+ * Returns true if the set is empty, false otherwise.
2286
+ *
2287
+ * @returns {boolean}
2288
+ */
2289
+ isEmpty:
2290
+ () => (elements.length <= 0),
2291
+
2292
+ /**
2293
+ * Returns true if the set contains a value that is equal to the given value, returns false otherwise.
2294
+ *
2295
+ * @param {*} value
2296
+ * @returns {boolean}
2297
+ */
2298
+ contains:
2299
+ (value) => binarySearch(value).found,
2300
+
2301
+ /**
2302
+ * Returns the first element of the set, or undefined if it is empty.
2303
+ *
2304
+ * @returns {*|undefined}
2305
+ */
2306
+ first:
2307
+ () => (elements.length > 0) ? elements[0] : undefined,
2308
+
2309
+ /**
2310
+ * Returns the last element of the set, or undefined if it is empty.
2311
+ *
2312
+ * @returns {*|undefined}
2313
+ */
2314
+ last:
2315
+ () => (elements.length > 0) ? elements[elements.length - 1] : undefined,
2316
+
2317
+ /**
2318
+ * Removes and returns the first element of the set, or undefined if it is empty.
2319
+ *
2320
+ * @returns {*|undefined}
2321
+ */
2322
+ pollFirst:
2323
+ () => (elements.length > 0) ? elements.splice(0, 1)[0] : undefined,
2324
+
2325
+ /**
2326
+ * Removes and returns the last element of the set, or undefined if it is empty.
2327
+ *
2328
+ * @returns {*|undefined}
2329
+ */
2330
+ pollLast:
2331
+ () => (elements.length > 0) ? elements.splice(elements.length - 1, 1)[0] : undefined,
2332
+
2333
+ /**
2334
+ * Adds the given value to the set. Will only do so if no equal value already exists.
2335
+ *
2336
+ * @param {*} value
2337
+ */
2338
+ add:
2339
+ (value) =>
2340
+ {
2341
+ const result = binarySearch(value);
2342
+ if(result.found)
2343
+ {
2344
+ return;
2345
+ }
2346
+ elements.splice(result.index, 0, value);
2347
+ },
2348
+
2349
+ /**
2350
+ * Adds all the given values to the set. Will only do so if no equal value already exists.
2351
+ *
2352
+ * @param {*[]|object} values
2353
+ */
2354
+ addAll:
2355
+ (values) =>
2356
+ {
2357
+ LeUtils.each(values, treeSet.add);
2358
+ },
2359
+
2360
+ /**
2361
+ * Returns an equal value that's already in the tree set, or undefined if no equal values in it exist.
2362
+ *
2363
+ * @param {*} value
2364
+ * @returns {*|undefined}
2365
+ */
2366
+ getEqualValue:
2367
+ (value) =>
2368
+ {
2369
+ const result = binarySearch(value);
2370
+ if(result.found)
2371
+ {
2372
+ return result.value;
2373
+ }
2374
+ return undefined;
2375
+ },
2376
+
2377
+ /**
2378
+ * Returns an equal value that's already in the tree set. If no equal values in it exist, the given value will be added and returned.
2379
+ *
2380
+ * @param {*} value
2381
+ * @returns {*}
2382
+ */
2383
+ getEqualValueOrAdd:
2384
+ (value) =>
2385
+ {
2386
+ const result = binarySearch(value);
2387
+ if(result.found)
2388
+ {
2389
+ return result.value;
2390
+ }
2391
+ elements.splice(result.index, 0, value);
2392
+ return value;
2393
+ },
2394
+ };
2395
+ return treeSet;
2396
+ },
2397
+
2398
+ /**
2399
+ * @typedef {Object} LeUtils~TransactionalValue
2400
+ * @property {*} value
2401
+ * @property {{id:string, value:*}[]} changes
2402
+ */
2403
+ /**
2404
+ * Creates and returns a new TransactionalValue object.
2405
+ * With a TransactionalValue, you can keep track of changes to a value, and commit or cancel them.
2406
+ *
2407
+ * Multiple uncommitted changes can be made at the same time, the last change will be the one that overwrites older changes.
2408
+ * If that change is cancelled, the previous change will be the one that overwrites older changes.
2409
+ * This allows you to make multiple unconfirmed changes, and confirm or cancel each of them individually at any time.
2410
+ *
2411
+ * @param {*} [value]
2412
+ * @returns {LeUtils~TransactionalValue}
2413
+ */
2414
+ createTransactionalValue:
2415
+ (value) =>
2416
+ {
2417
+ if(typeof value === 'undefined')
2418
+ {
2419
+ value = null;
2420
+ }
2421
+ return {value:value, changes:[]};
2422
+ },
2423
+
2424
+ /**
2425
+ * Returns true if the given value is a valid TransactionalValue, returns false if it isn't.
2426
+ *
2427
+ * @param {LeUtils~TransactionalValue} transactionalValue
2428
+ * @returns {boolean}
2429
+ */
2430
+ isTransactionalValueValid:
2431
+ (transactionalValue) =>
2432
+ {
2433
+ return ((typeof transactionalValue === 'object') && ('value' in transactionalValue) && ('changes' in transactionalValue) && Array.isArray(transactionalValue.changes));
2434
+ },
2435
+
2436
+ /**
2437
+ * Returns true if the given value is a TransactionalValue, false otherwise.
2438
+ *
2439
+ * @param {LeUtils~TransactionalValue} transactionalValue
2440
+ * @returns {string}
2441
+ */
2442
+ transactionalValueToString:
2443
+ (transactionalValue) =>
2444
+ {
2445
+ if(!LeUtils.isTransactionalValueValid(transactionalValue))
2446
+ {
2447
+ return transactionalValue + '';
2448
+ }
2449
+ if(transactionalValue.changes.length <= 0)
2450
+ {
2451
+ return '' + transactionalValue.value;
2452
+ }
2453
+ let valuesString = '' + transactionalValue.value;
2454
+ for(let i = 0; i < transactionalValue.changes.length; i++)
2455
+ {
2456
+ valuesString += ' -> ' + transactionalValue.changes[i].value;
2457
+ }
2458
+ return transactionalValue.changes[transactionalValue.changes.length - 1].value + ' (' + valuesString + ')';
2459
+ },
2460
+
2461
+ /**
2462
+ * Sets the committed value of the given TransactionalValue to the given value. Clears out the previously uncommitted changes.
2463
+ *
2464
+ * @param {LeUtils~TransactionalValue} transactionalValue
2465
+ * @param {*} value
2466
+ */
2467
+ transactionSetAndCommit:
2468
+ (transactionalValue, value) =>
2469
+ {
2470
+ checkTransactionalValue(transactionalValue);
2471
+ if(typeof value === 'undefined')
2472
+ {
2473
+ value = null;
2474
+ }
2475
+ transactionalValue.value = value;
2476
+ transactionalValue.changes = [];
2477
+ },
2478
+
2479
+ /**
2480
+ * Sets the value of the given TransactionalValue to the given value, without yet committing it, meaning it can be committed or cancelled later.
2481
+ * It returns the ID of the change, which can be used to commit or cancel the change later.
2482
+ *
2483
+ * @param {LeUtils~TransactionalValue} transactionalValue
2484
+ * @param {*} value
2485
+ * @returns {string}
2486
+ */
2487
+ transactionSetWithoutCommitting:
2488
+ (transactionalValue, value) =>
2489
+ {
2490
+ checkTransactionalValue(transactionalValue);
2491
+ if(typeof value === 'undefined')
2492
+ {
2493
+ value = null;
2494
+ }
2495
+ const id = LeUtils.uniqueId();
2496
+ transactionalValue.changes.push({id:id, value:value});
2497
+ return id;
2498
+ },
2499
+
2500
+ /**
2501
+ * Commits the change with the given ID, making it the new committed value.
2502
+ * Returns true if the change was found and committed, returns false if it was already overwritten by a newer committed change.
2503
+ *
2504
+ * @param {LeUtils~TransactionalValue} transactionalValue
2505
+ * @param {string} changeId
2506
+ * @returns {boolean}
2507
+ */
2508
+ transactionCommitChange:
2509
+ (transactionalValue, changeId) =>
2510
+ {
2511
+ checkTransactionalValue(transactionalValue);
2512
+ const change = findTransactionalValueChange(transactionalValue, changeId);
2513
+ if(change === null)
2514
+ {
2515
+ return false;
2516
+ }
2517
+ transactionalValue.value = change.value;
2518
+ transactionalValue.changes.splice(0, change.index + 1);
2519
+ return true;
2520
+ },
2521
+
2522
+ /**
2523
+ * Cancels the change with the given ID, removing it from the uncommitted changes.
2524
+ * Returns true if the change was found and removed, returns false if it was already overwritten by a newer committed change.
2525
+ *
2526
+ * @param {LeUtils~TransactionalValue} transactionalValue
2527
+ * @param {string} changeId
2528
+ * @returns {boolean}
2529
+ */
2530
+ transactionCancelChange:
2531
+ (transactionalValue, changeId) =>
2532
+ {
2533
+ checkTransactionalValue(transactionalValue);
2534
+ const change = findTransactionalValueChange(transactionalValue, changeId);
2535
+ if(change === null)
2536
+ {
2537
+ return false;
2538
+ }
2539
+ transactionalValue.changes.splice(change.index, 1);
2540
+ return true;
2541
+ },
2542
+
2543
+ /**
2544
+ * Returns true if the change was found, meaning it can still make a difference to the final committed value of this TransactionalValue.
2545
+ * 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.
2546
+ *
2547
+ * @param {LeUtils~TransactionalValue} transactionalValue
2548
+ * @param {string} changeId
2549
+ * @returns {boolean}
2550
+ */
2551
+ transactionIsChangeRelevant:
2552
+ (transactionalValue, changeId) =>
2553
+ {
2554
+ checkTransactionalValue(transactionalValue);
2555
+ return (findTransactionalValueChange(transactionalValue, changeId) !== null);
2556
+ },
2557
+
2558
+ /**
2559
+ * Returns the committed value of the given TransactionalValue.
2560
+ *
2561
+ * @param {LeUtils~TransactionalValue} transactionalValue
2562
+ * @returns {*}
2563
+ */
2564
+ transactionGetCommittedValue:
2565
+ (transactionalValue) =>
2566
+ {
2567
+ checkTransactionalValue(transactionalValue);
2568
+ return transactionalValue.value;
2569
+ },
2570
+
2571
+ /**
2572
+ * Returns the value (including any uncommitted changes made to it) of the given TransactionalValue.
2573
+ *
2574
+ * @param {LeUtils~TransactionalValue} transactionalValue
2575
+ * @returns {*}
2576
+ */
2577
+ transactionGetValue:
2578
+ (transactionalValue) =>
2579
+ {
2580
+ checkTransactionalValue(transactionalValue);
2581
+ if(transactionalValue.changes.length <= 0)
2582
+ {
2583
+ return transactionalValue.value;
2584
+ }
2585
+ return transactionalValue.changes[transactionalValue.changes.length - 1].value;
2586
+ },
890
2587
  };