UNPKG

15.2 kBJavaScriptView Raw
1/**
2 * Flatten array, one level deep.
3 *
4 * @template T
5 *
6 * @param {T[][] | T[] | null} [arr]
7 *
8 * @return {T[]}
9 */
10function flatten(arr) {
11 return Array.prototype.concat.apply([], arr);
12}
13
14const nativeToString = Object.prototype.toString;
15const nativeHasOwnProperty = Object.prototype.hasOwnProperty;
16
17function isUndefined(obj) {
18 return obj === undefined;
19}
20
21function isDefined(obj) {
22 return obj !== undefined;
23}
24
25function isNil(obj) {
26 return obj == null;
27}
28
29function isArray(obj) {
30 return nativeToString.call(obj) === '[object Array]';
31}
32
33function isObject(obj) {
34 return nativeToString.call(obj) === '[object Object]';
35}
36
37function isNumber(obj) {
38 return nativeToString.call(obj) === '[object Number]';
39}
40
41/**
42 * @param {any} obj
43 *
44 * @return {boolean}
45 */
46function isFunction(obj) {
47 const tag = nativeToString.call(obj);
48
49 return (
50 tag === '[object Function]' ||
51 tag === '[object AsyncFunction]' ||
52 tag === '[object GeneratorFunction]' ||
53 tag === '[object AsyncGeneratorFunction]' ||
54 tag === '[object Proxy]'
55 );
56}
57
58function isString(obj) {
59 return nativeToString.call(obj) === '[object String]';
60}
61
62
63/**
64 * Ensure collection is an array.
65 *
66 * @param {Object} obj
67 */
68function ensureArray(obj) {
69
70 if (isArray(obj)) {
71 return;
72 }
73
74 throw new Error('must supply array');
75}
76
77/**
78 * Return true, if target owns a property with the given key.
79 *
80 * @param {Object} target
81 * @param {String} key
82 *
83 * @return {Boolean}
84 */
85function has(target, key) {
86 return nativeHasOwnProperty.call(target, key);
87}
88
89/**
90 * @template T
91 * @typedef { (
92 * ((e: T) => boolean) |
93 * ((e: T, idx: number) => boolean) |
94 * ((e: T, key: string) => boolean) |
95 * string |
96 * number
97 * ) } Matcher
98 */
99
100/**
101 * @template T
102 * @template U
103 *
104 * @typedef { (
105 * ((e: T) => U) | string | number
106 * ) } Extractor
107 */
108
109
110/**
111 * @template T
112 * @typedef { (val: T, key: any) => boolean } MatchFn
113 */
114
115/**
116 * @template T
117 * @typedef { T[] } ArrayCollection
118 */
119
120/**
121 * @template T
122 * @typedef { { [key: string]: T } } StringKeyValueCollection
123 */
124
125/**
126 * @template T
127 * @typedef { { [key: number]: T } } NumberKeyValueCollection
128 */
129
130/**
131 * @template T
132 * @typedef { StringKeyValueCollection<T> | NumberKeyValueCollection<T> } KeyValueCollection
133 */
134
135/**
136 * @template T
137 * @typedef { KeyValueCollection<T> | ArrayCollection<T> } Collection
138 */
139
140/**
141 * Find element in collection.
142 *
143 * @template T
144 * @param {Collection<T>} collection
145 * @param {Matcher<T>} matcher
146 *
147 * @return {Object}
148 */
149function find(collection, matcher) {
150
151 const matchFn = toMatcher(matcher);
152
153 let match;
154
155 forEach(collection, function(val, key) {
156 if (matchFn(val, key)) {
157 match = val;
158
159 return false;
160 }
161 });
162
163 return match;
164
165}
166
167
168/**
169 * Find element index in collection.
170 *
171 * @template T
172 * @param {Collection<T>} collection
173 * @param {Matcher<T>} matcher
174 *
175 * @return {number}
176 */
177function findIndex(collection, matcher) {
178
179 const matchFn = toMatcher(matcher);
180
181 let idx = isArray(collection) ? -1 : undefined;
182
183 forEach(collection, function(val, key) {
184 if (matchFn(val, key)) {
185 idx = key;
186
187 return false;
188 }
189 });
190
191 return idx;
192}
193
194
195/**
196 * Filter elements in collection.
197 *
198 * @template T
199 * @param {Collection<T>} collection
200 * @param {Matcher<T>} matcher
201 *
202 * @return {T[]} result
203 */
204function filter(collection, matcher) {
205
206 const matchFn = toMatcher(matcher);
207
208 let result = [];
209
210 forEach(collection, function(val, key) {
211 if (matchFn(val, key)) {
212 result.push(val);
213 }
214 });
215
216 return result;
217}
218
219
220/**
221 * Iterate over collection; returning something
222 * (non-undefined) will stop iteration.
223 *
224 * @template T
225 * @param {Collection<T>} collection
226 * @param { ((item: T, idx: number) => (boolean|void)) | ((item: T, key: string) => (boolean|void)) } iterator
227 *
228 * @return {T} return result that stopped the iteration
229 */
230function forEach(collection, iterator) {
231
232 let val,
233 result;
234
235 if (isUndefined(collection)) {
236 return;
237 }
238
239 const convertKey = isArray(collection) ? toNum : identity;
240
241 for (let key in collection) {
242
243 if (has(collection, key)) {
244 val = collection[key];
245
246 result = iterator(val, convertKey(key));
247
248 if (result === false) {
249 return val;
250 }
251 }
252 }
253}
254
255/**
256 * Return collection without element.
257 *
258 * @template T
259 * @param {ArrayCollection<T>} arr
260 * @param {Matcher<T>} matcher
261 *
262 * @return {T[]}
263 */
264function without(arr, matcher) {
265
266 if (isUndefined(arr)) {
267 return [];
268 }
269
270 ensureArray(arr);
271
272 const matchFn = toMatcher(matcher);
273
274 return arr.filter(function(el, idx) {
275 return !matchFn(el, idx);
276 });
277
278}
279
280
281/**
282 * Reduce collection, returning a single result.
283 *
284 * @template T
285 * @template V
286 *
287 * @param {Collection<T>} collection
288 * @param {(result: V, entry: T, index: any) => V} iterator
289 * @param {V} result
290 *
291 * @return {V} result returned from last iterator
292 */
293function reduce(collection, iterator, result) {
294
295 forEach(collection, function(value, idx) {
296 result = iterator(result, value, idx);
297 });
298
299 return result;
300}
301
302
303/**
304 * Return true if every element in the collection
305 * matches the criteria.
306 *
307 * @param {Object|Array} collection
308 * @param {Function} matcher
309 *
310 * @return {Boolean}
311 */
312function every(collection, matcher) {
313
314 return !!reduce(collection, function(matches, val, key) {
315 return matches && matcher(val, key);
316 }, true);
317}
318
319
320/**
321 * Return true if some elements in the collection
322 * match the criteria.
323 *
324 * @param {Object|Array} collection
325 * @param {Function} matcher
326 *
327 * @return {Boolean}
328 */
329function some(collection, matcher) {
330
331 return !!find(collection, matcher);
332}
333
334
335/**
336 * Transform a collection into another collection
337 * by piping each member through the given fn.
338 *
339 * @param {Object|Array} collection
340 * @param {Function} fn
341 *
342 * @return {Array} transformed collection
343 */
344function map(collection, fn) {
345
346 let result = [];
347
348 forEach(collection, function(val, key) {
349 result.push(fn(val, key));
350 });
351
352 return result;
353}
354
355
356/**
357 * Get the collections keys.
358 *
359 * @param {Object|Array} collection
360 *
361 * @return {Array}
362 */
363function keys(collection) {
364 return collection && Object.keys(collection) || [];
365}
366
367
368/**
369 * Shorthand for `keys(o).length`.
370 *
371 * @param {Object|Array} collection
372 *
373 * @return {Number}
374 */
375function size(collection) {
376 return keys(collection).length;
377}
378
379
380/**
381 * Get the values in the collection.
382 *
383 * @param {Object|Array} collection
384 *
385 * @return {Array}
386 */
387function values(collection) {
388 return map(collection, (val) => val);
389}
390
391
392/**
393 * Group collection members by attribute.
394 *
395 * @param {Object|Array} collection
396 * @param {Extractor} extractor
397 *
398 * @return {Object} map with { attrValue => [ a, b, c ] }
399 */
400function groupBy(collection, extractor, grouped = {}) {
401
402 extractor = toExtractor(extractor);
403
404 forEach(collection, function(val) {
405 let discriminator = extractor(val) || '_';
406
407 let group = grouped[discriminator];
408
409 if (!group) {
410 group = grouped[discriminator] = [];
411 }
412
413 group.push(val);
414 });
415
416 return grouped;
417}
418
419
420function uniqueBy(extractor, ...collections) {
421
422 extractor = toExtractor(extractor);
423
424 let grouped = {};
425
426 forEach(collections, (c) => groupBy(c, extractor, grouped));
427
428 let result = map(grouped, function(val, key) {
429 return val[0];
430 });
431
432 return result;
433}
434
435
436const unionBy = uniqueBy;
437
438
439
440/**
441 * Sort collection by criteria.
442 *
443 * @template T
444 *
445 * @param {Collection<T>} collection
446 * @param {Extractor<T, number | string>} extractor
447 *
448 * @return {Array}
449 */
450function sortBy(collection, extractor) {
451
452 extractor = toExtractor(extractor);
453
454 let sorted = [];
455
456 forEach(collection, function(value, key) {
457 let disc = extractor(value, key);
458
459 let entry = {
460 d: disc,
461 v: value
462 };
463
464 for (var idx = 0; idx < sorted.length; idx++) {
465 let { d } = sorted[idx];
466
467 if (disc < d) {
468 sorted.splice(idx, 0, entry);
469 return;
470 }
471 }
472
473 // not inserted, append (!)
474 sorted.push(entry);
475 });
476
477 return map(sorted, (e) => e.v);
478}
479
480
481/**
482 * Create an object pattern matcher.
483 *
484 * @example
485 *
486 * ```javascript
487 * const matcher = matchPattern({ id: 1 });
488 *
489 * let element = find(elements, matcher);
490 * ```
491 *
492 * @template T
493 *
494 * @param {T} pattern
495 *
496 * @return { (el: any) => boolean } matcherFn
497 */
498function matchPattern(pattern) {
499
500 return function(el) {
501
502 return every(pattern, function(val, key) {
503 return el[key] === val;
504 });
505
506 };
507}
508
509
510/**
511 * @param {string | ((e: any) => any) } extractor
512 *
513 * @return { (e: any) => any }
514 */
515function toExtractor(extractor) {
516
517 /**
518 * @satisfies { (e: any) => any }
519 */
520 return isFunction(extractor) ? extractor : (e) => {
521
522 // @ts-ignore: just works
523 return e[extractor];
524 };
525}
526
527
528/**
529 * @template T
530 * @param {Matcher<T>} matcher
531 *
532 * @return {MatchFn<T>}
533 */
534function toMatcher(matcher) {
535 return isFunction(matcher) ? matcher : (e) => {
536 return e === matcher;
537 };
538}
539
540
541function identity(arg) {
542 return arg;
543}
544
545function toNum(arg) {
546 return Number(arg);
547}
548
549/* global setTimeout clearTimeout */
550
551/**
552 * @typedef { {
553 * (...args: any[]): any;
554 * flush: () => void;
555 * cancel: () => void;
556 * } } DebouncedFunction
557 */
558
559/**
560 * Debounce fn, calling it only once if the given time
561 * elapsed between calls.
562 *
563 * Lodash-style the function exposes methods to `#clear`
564 * and `#flush` to control internal behavior.
565 *
566 * @param {Function} fn
567 * @param {Number} timeout
568 *
569 * @return {DebouncedFunction} debounced function
570 */
571function debounce(fn, timeout) {
572
573 let timer;
574
575 let lastArgs;
576 let lastThis;
577
578 let lastNow;
579
580 function fire(force) {
581
582 let now = Date.now();
583
584 let scheduledDiff = force ? 0 : (lastNow + timeout) - now;
585
586 if (scheduledDiff > 0) {
587 return schedule(scheduledDiff);
588 }
589
590 fn.apply(lastThis, lastArgs);
591
592 clear();
593 }
594
595 function schedule(timeout) {
596 timer = setTimeout(fire, timeout);
597 }
598
599 function clear() {
600 if (timer) {
601 clearTimeout(timer);
602 }
603
604 timer = lastNow = lastArgs = lastThis = undefined;
605 }
606
607 function flush() {
608 if (timer) {
609 fire(true);
610 }
611
612 clear();
613 }
614
615 /**
616 * @type { DebouncedFunction }
617 */
618 function callback(...args) {
619 lastNow = Date.now();
620
621 lastArgs = args;
622 lastThis = this;
623
624 // ensure an execution is scheduled
625 if (!timer) {
626 schedule(timeout);
627 }
628 }
629
630 callback.flush = flush;
631 callback.cancel = clear;
632
633 return callback;
634}
635
636/**
637 * Throttle fn, calling at most once
638 * in the given interval.
639 *
640 * @param {Function} fn
641 * @param {Number} interval
642 *
643 * @return {Function} throttled function
644 */
645function throttle(fn, interval) {
646 let throttling = false;
647
648 return function(...args) {
649
650 if (throttling) {
651 return;
652 }
653
654 fn(...args);
655 throttling = true;
656
657 setTimeout(() => {
658 throttling = false;
659 }, interval);
660 };
661}
662
663/**
664 * Bind function against target <this>.
665 *
666 * @param {Function} fn
667 * @param {Object} target
668 *
669 * @return {Function} bound function
670 */
671function bind(fn, target) {
672 return fn.bind(target);
673}
674
675/**
676 * Convenience wrapper for `Object.assign`.
677 *
678 * @param {Object} target
679 * @param {...Object} others
680 *
681 * @return {Object} the target
682 */
683function assign(target, ...others) {
684 return Object.assign(target, ...others);
685}
686
687/**
688 * Sets a nested property of a given object to the specified value.
689 *
690 * This mutates the object and returns it.
691 *
692 * @template T
693 *
694 * @param {T} target The target of the set operation.
695 * @param {(string|number)[]} path The path to the nested value.
696 * @param {any} value The value to set.
697 *
698 * @return {T}
699 */
700function set(target, path, value) {
701
702 let currentTarget = target;
703
704 forEach(path, function(key, idx) {
705
706 if (typeof key !== 'number' && typeof key !== 'string') {
707 throw new Error('illegal key type: ' + typeof key + '. Key should be of type number or string.');
708 }
709
710 if (key === 'constructor') {
711 throw new Error('illegal key: constructor');
712 }
713
714 if (key === '__proto__') {
715 throw new Error('illegal key: __proto__');
716 }
717
718 let nextKey = path[idx + 1];
719 let nextTarget = currentTarget[key];
720
721 if (isDefined(nextKey) && isNil(nextTarget)) {
722 nextTarget = currentTarget[key] = isNaN(+nextKey) ? {} : [];
723 }
724
725 if (isUndefined(nextKey)) {
726 if (isUndefined(value)) {
727 delete currentTarget[key];
728 } else {
729 currentTarget[key] = value;
730 }
731 } else {
732 currentTarget = nextTarget;
733 }
734 });
735
736 return target;
737}
738
739
740/**
741 * Gets a nested property of a given object.
742 *
743 * @param {Object} target The target of the get operation.
744 * @param {(string|number)[]} path The path to the nested value.
745 * @param {any} [defaultValue] The value to return if no value exists.
746 *
747 * @return {any}
748 */
749function get(target, path, defaultValue) {
750
751 let currentTarget = target;
752
753 forEach(path, function(key) {
754
755 // accessing nil property yields <undefined>
756 if (isNil(currentTarget)) {
757 currentTarget = undefined;
758
759 return false;
760 }
761
762 currentTarget = currentTarget[key];
763 });
764
765 return isUndefined(currentTarget) ? defaultValue : currentTarget;
766}
767
768/**
769 * Pick properties from the given target.
770 *
771 * @template T
772 * @template {any[]} V
773 *
774 * @param {T} target
775 * @param {V} properties
776 *
777 * @return Pick<T, V>
778 */
779function pick(target, properties) {
780
781 let result = {};
782
783 let obj = Object(target);
784
785 forEach(properties, function(prop) {
786
787 if (prop in obj) {
788 result[prop] = target[prop];
789 }
790 });
791
792 return result;
793}
794
795/**
796 * Pick all target properties, excluding the given ones.
797 *
798 * @template T
799 * @template {any[]} V
800 *
801 * @param {T} target
802 * @param {V} properties
803 *
804 * @return {Omit<T, V>} target
805 */
806function omit(target, properties) {
807
808 let result = {};
809
810 let obj = Object(target);
811
812 forEach(obj, function(prop, key) {
813
814 if (properties.indexOf(key) === -1) {
815 result[key] = prop;
816 }
817 });
818
819 return result;
820}
821
822/**
823 * Recursively merge `...sources` into given target.
824 *
825 * Does support merging objects; does not support merging arrays.
826 *
827 * @param {Object} target
828 * @param {...Object} sources
829 *
830 * @return {Object} the target
831 */
832function merge(target, ...sources) {
833
834 if (!sources.length) {
835 return target;
836 }
837
838 forEach(sources, function(source) {
839
840 // skip non-obj sources, i.e. null
841 if (!source || !isObject(source)) {
842 return;
843 }
844
845 forEach(source, function(sourceVal, key) {
846
847 if (key === '__proto__') {
848 return;
849 }
850
851 let targetVal = target[key];
852
853 if (isObject(sourceVal)) {
854
855 if (!isObject(targetVal)) {
856
857 // override target[key] with object
858 targetVal = {};
859 }
860
861 target[key] = merge(targetVal, sourceVal);
862 } else {
863 target[key] = sourceVal;
864 }
865
866 });
867 });
868
869 return target;
870}
871
872export { assign, bind, debounce, ensureArray, every, filter, find, findIndex, flatten, forEach, get, groupBy, has, isArray, isDefined, isFunction, isNil, isNumber, isObject, isString, isUndefined, keys, map, matchPattern, merge, omit, pick, reduce, set, size, some, sortBy, throttle, unionBy, uniqueBy, values, without };