@slickgrid-universal/utils 5.0.0-beta.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.ts CHANGED
@@ -1,418 +1,389 @@
1
- import type { AnyFunction } from './models/types';
2
-
3
- /**
4
- * Add an item to an array only when the item does not exists, when the item is an object we will be using their "id" to compare
5
- * @param inputArray
6
- * @param inputItem
7
- * @param itemIdPropName
8
- */
9
- export function addToArrayWhenNotExists<T = any>(inputArray: T[], inputItem: T, itemIdPropName = 'id') {
10
- let arrayRowIndex = -1;
11
- if (inputItem && typeof inputItem === 'object' && itemIdPropName in inputItem) {
12
- arrayRowIndex = inputArray.findIndex((item) => item[itemIdPropName as keyof T] === inputItem[itemIdPropName as keyof T]);
13
- } else {
14
- arrayRowIndex = inputArray.findIndex((item) => item === inputItem);
15
- }
16
-
17
- if (arrayRowIndex < 0) {
18
- inputArray.push(inputItem);
19
- }
20
- }
21
-
22
- /**
23
- * Simple function that will return a string with the number of whitespaces asked, mostly used by the CSV export
24
- * @param {Number} nbSpaces - number of white spaces to create
25
- * @param {String} spaceChar - optionally provide a different character to use for the whitespace (e.g. could be override to use "&nbsp;" in html)
26
- */
27
- export function addWhiteSpaces(nbSpaces: number, spaceChar = ' '): string {
28
- let result = '';
29
-
30
- for (let i = 0; i < nbSpaces; i++) {
31
- result += spaceChar;
32
- }
33
- return result;
34
- }
35
-
36
- /**
37
- * Remove an item from the array by its index
38
- * @param array input
39
- * @param index
40
- */
41
- export function arrayRemoveItemByIndex<T>(array: T[], index: number): T[] {
42
- return array.filter((_el: T, i: number) => index !== i);
43
- }
44
-
45
- /**
46
- * Create an immutable clone of an array or object
47
- * (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com
48
- * @param {Array|Object} objectOrArray - the array or object to copy
49
- * @return {Array|Object} - the clone of the array or object
50
- */
51
- export function deepCopy(objectOrArray: any | any[]): any | any[] {
52
- /**
53
- * Create an immutable copy of an object
54
- * @return {Object}
55
- */
56
- const cloneObj = () => {
57
- // Create new object
58
- const clone = {};
59
-
60
- // Loop through each item in the original
61
- // Recursively copy it's value and add to the clone
62
- Object.keys(objectOrArray).forEach(key => {
63
- if (Object.prototype.hasOwnProperty.call(objectOrArray, key)) {
64
- (clone as any)[key] = deepCopy(objectOrArray[key]);
65
- }
66
- });
67
- return clone;
68
- };
69
-
70
- /**
71
- * Create an immutable copy of an array
72
- * @return {Array}
73
- */
74
- const cloneArr = () => objectOrArray.map((item: any) => deepCopy(item));
75
-
76
- // -- init --//
77
- // Get object type
78
- const type = Object.prototype.toString.call(objectOrArray).slice(8, -1).toLowerCase();
79
-
80
- // If an object
81
- if (type === 'object') {
82
- return cloneObj();
83
- }
84
- // If an array
85
- if (type === 'array') {
86
- return cloneArr();
87
- }
88
- // Otherwise, return it as-is
89
- return objectOrArray;
90
- }
91
-
92
- /**
93
- * Performs a deep merge of objects and returns new object, it does not modify the source object, objects (immutable) and merges arrays via concatenation.
94
- * Also, if first argument is undefined/null but next argument is an object then it will proceed and output will be an object
95
- * @param {Object} target - the target object — what to apply the sources' properties to, which is returned after it is modified.
96
- * @param {Object} sources - the source object(s) — objects containing the properties you want to apply.
97
- * @returns {Object} The target object.
98
- */
99
- export function deepMerge(target: any, ...sources: any[]): any {
100
- if (!sources.length) {
101
- return target;
102
- }
103
- const source = sources.shift();
104
-
105
- // when target is not an object but source is an object, then we'll assign as object
106
- target = (!isObject(target) && isObject(source)) ? {} : target;
107
-
108
- if (isObject(target) && isObject(source)) {
109
- Object.keys(source).forEach(prop => {
110
- if (source.hasOwnProperty(prop)) {
111
- if (prop in target) {
112
- // handling merging of two properties with equal names
113
- if (typeof target[prop] !== 'object') {
114
- target[prop] = source[prop];
115
- } else {
116
- if (typeof source[prop] !== 'object') {
117
- target[prop] = source[prop];
118
- } else {
119
- if (target[prop].concat && source[prop].concat) {
120
- // two arrays get concatenated
121
- target[prop] = target[prop].concat(source[prop]);
122
- } else {
123
- // two objects get merged recursively
124
- target[prop] = deepMerge(target[prop], source[prop]);
125
- }
126
- }
127
- }
128
- } else {
129
- // new properties get added to target
130
- target[prop] = source[prop];
131
- }
132
- }
133
- });
134
- }
135
- return deepMerge(target, ...sources);
136
- }
137
-
138
- /**
139
- * Empty an object properties by looping through them all and deleting them
140
- * @param obj - input object
141
- */
142
- export function emptyObject(obj: any) {
143
- if (isObject(obj)) {
144
- Object.keys(obj).forEach(key => {
145
- if (obj.hasOwnProperty(key)) {
146
- delete obj[key];
147
- }
148
- });
149
- }
150
- obj = null;
151
- obj = {};
152
-
153
- return obj;
154
- }
155
-
156
- /**
157
- * Get the function details (param & body) of a function.
158
- * It supports regular function and also ES6 arrow functions
159
- * @param {Function} fn - function to analyze
160
- * @param {Boolean} [addReturn] - when using ES6 function as single liner, we could add the missing `return ...`
161
- * @returns
162
- */
163
- export function getFunctionDetails(fn: AnyFunction, addReturn = true) {
164
- let isAsyncFn = false;
165
-
166
- const getFunctionBody = (func: AnyFunction) => {
167
- const fnStr = func.toString();
168
- isAsyncFn = fnStr.includes('async ');
169
-
170
- // when fn is one liner arrow fn returning an object in brackets e.g. `() => ({ hello: 'world' })`
171
- if ((fnStr.replaceAll(' ', '').includes('=>({'))) {
172
- const matches = fnStr.match(/(({.*}))/g) || [];
173
- return matches.length >= 1 ? `return ${matches[0]!.trimStart()}` : fnStr;
174
- }
175
- const isOneLinerArrowFn = (!fnStr.includes('{') && fnStr.includes('=>'));
176
- const body = fnStr.substring(
177
- (fnStr.indexOf('{') + 1) || (fnStr.indexOf('=>') + 2),
178
- fnStr.includes('}') ? fnStr.lastIndexOf('}') : fnStr.length
179
- );
180
- if (addReturn && isOneLinerArrowFn && !body.startsWith('return')) {
181
- return 'return ' + body.trimStart(); // add the `return ...` to the body for ES6 arrow fn
182
- }
183
- return body;
184
- };
185
-
186
- const getFunctionParams = (func: AnyFunction): string[] => {
187
- const STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,)]*))/mg;
188
- const ARG_NAMES = /([^\s,]+)/g;
189
- const fnStr = func.toString().replace(STRIP_COMMENTS, '');
190
- return fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARG_NAMES) ?? [];
191
- };
192
-
193
- return {
194
- params: getFunctionParams(fn),
195
- body: getFunctionBody(fn),
196
- isAsync: isAsyncFn,
197
- };
198
- }
199
-
200
- /**
201
- * Check if an object is empty
202
- * @param obj - input object
203
- * @returns - boolean
204
- */
205
- export function isEmptyObject(obj: any): boolean {
206
- if (obj === null || obj === undefined) {
207
- return true;
208
- }
209
- return Object.entries(obj).length === 0;
210
- }
211
-
212
- export function isDefined<T>(value: T | undefined | null): value is T {
213
- return <T>value !== undefined && <T>value !== null && <T>value !== '';
214
- }
215
-
216
- export function isDefinedNumber<T>(value: T | undefined | null): value is T {
217
- return <T>value !== null && !isNaN(value as any) && <T>value !== '';
218
- }
219
-
220
- /**
221
- * Simple object check.
222
- * @param item
223
- * @returns {boolean}
224
- */
225
- export function isObject(item: any) {
226
- return item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date);
227
- }
228
-
229
- /**
230
- * Simple check to detect if the value is a primitive type
231
- * @param val
232
- * @returns {boolean}
233
- */
234
- export function isPrimitiveValue(val: any) {
235
- return typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string' || val === null || val === undefined;
236
- }
237
-
238
- export function isPrimitiveOrHTML(val: any) {
239
- return val instanceof HTMLElement || val instanceof DocumentFragment || isPrimitiveValue(val);
240
- }
241
-
242
- /**
243
- * Check if a value has any data (undefined, null or empty string will return False...)
244
- * NOTE: a `false` boolean is consider as having data so it will return True
245
- */
246
- export function hasData(value: any): boolean {
247
- return value !== undefined && value !== null && value !== '';
248
- }
249
-
250
- /**
251
- * Check if input value is a number, by default it won't be a strict checking
252
- * but optionally we could check for strict equality, for example "3" in strict mode would return False but True when non-strict.
253
- * @param value - input value of any type
254
- * @param strict - when using strict it also check for strict equality, e.g "3" in strict mode would return False but True when non-strict
255
- */
256
- export function isNumber(value: any, strict = false) {
257
- if (strict) {
258
- return (value === null || value === undefined || typeof value === 'string') ? false : !isNaN(value);
259
- }
260
- return (value === null || value === undefined || value === '') ? false : !isNaN(+value);
261
- }
262
-
263
- /** Check if an object is empty, it will also be considered empty when the input is null, undefined or isn't an object */
264
- export function isObjectEmpty(obj: unknown) {
265
- return !obj || (obj && typeof obj === 'object' && Object.keys(obj).length === 0);
266
- }
267
-
268
- /** Parse any input (bool, number, string) and return a boolean or False when not possible */
269
- export function parseBoolean(input: any): boolean {
270
- return /(true|1)/i.test(input + '');
271
- }
272
-
273
- /**
274
- * Remove any accents from a string by normalizing it
275
- * @param {String} text - input text
276
- * @param {Boolean} shouldLowerCase - should we also lowercase the string output?
277
- * @returns
278
- */
279
- export function removeAccentFromText(text: string, shouldLowerCase = false) {
280
- const normalizedText = (typeof text.normalize === 'function') ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
281
- return shouldLowerCase ? normalizedText.toLowerCase() : normalizedText;
282
- }
283
-
284
- /** Set the object value of deeper node from a given dot (.) notation path (e.g.: "user.firstName") */
285
- export function setDeepValue<T = unknown>(obj: T, path: string | string[], value: any) {
286
- if (typeof path === 'string') {
287
- path = path.split('.');
288
- }
289
-
290
- if (path.length > 1) {
291
- const e = path.shift() as keyof T;
292
- if (obj && e !== undefined) {
293
- setDeepValue(
294
- (obj)[e] = (hasData(obj[e]) && (Array.isArray(obj[e]) || Object.prototype.toString.call((obj)[e]) === '[object Object]'))
295
- ? (obj)[e]
296
- : {} as any,
297
- path,
298
- value
299
- );
300
- }
301
- } else if (obj && path[0]) {
302
- (obj)[path[0] as keyof T] = value;
303
- }
304
- }
305
-
306
- /**
307
- * Title case (or capitalize) first char of a string, for example "hello world" will become "Hello world"
308
- * Change the string to be title case on the complete sentence (upper case first char of each word while changing everything else to lower case)
309
- * @param inputStr
310
- * @returns string
311
- */
312
- export function titleCase(inputStr: string, shouldTitleCaseEveryWords = false): string {
313
- if (typeof inputStr === 'string') {
314
- if (shouldTitleCaseEveryWords) {
315
- return inputStr.replace(/\w\S*/g, (outputStr) => {
316
- return outputStr.charAt(0).toUpperCase() + outputStr.substring(1).toLowerCase();
317
- });
318
- }
319
- return inputStr.charAt(0).toUpperCase() + inputStr.slice(1);
320
- }
321
- return inputStr;
322
- }
323
-
324
- /**
325
- * Converts a string to camel case (camelCase), for example "hello-world" (or "hellow world") will become "helloWorld"
326
- * @param inputStr the string to convert
327
- * @return the string in camel case
328
- */
329
- export function toCamelCase(inputStr: string): string {
330
- if (typeof inputStr === 'string') {
331
- return inputStr.replace(/(?:^\w|[A-Z]|\b\w|[\s+\-_/])/g, (match: string, offset: number) => {
332
- // remove white space or hypens or underscores
333
- if (/[\s+\-_/]/.test(match)) {
334
- return '';
335
- }
336
-
337
- return offset === 0 ? match.toLowerCase() : match.toUpperCase();
338
- });
339
- }
340
- return inputStr;
341
- }
342
-
343
- /**
344
- * Converts a string to kebab (hypen) case, for example "helloWorld" will become "hello-world"
345
- * @param str the string to convert
346
- * @return the string in kebab case
347
- */
348
- export function toKebabCase(inputStr: string): string {
349
- if (typeof inputStr === 'string') {
350
- return toCamelCase(inputStr).replace(/([A-Z])/g, '-$1').toLowerCase();
351
- }
352
- return inputStr;
353
- }
354
-
355
- /**
356
- * Converts a camelCase or kebab-case string to a sentence case, for example "helloWorld" will become "Hello World" and "hello-world" will become "Hello world"
357
- * @param str the string to convert
358
- * @return the string in kebab case
359
- */
360
- export function toSentenceCase(inputStr: string): string {
361
- if (typeof inputStr === 'string') {
362
- const result = inputStr.replace(/([A-Z])|(-)/g, ' $1').replace(/\s+/g, ' ').trim();
363
- return result.charAt(0).toUpperCase() + result.slice(1);
364
- }
365
- return inputStr;
366
- }
367
-
368
- /**
369
- * Converts a string from camelCase to snake_case (underscore) case
370
- * @param str the string to convert
371
- * @return the string in kebab case
372
- */
373
- export function toSnakeCase(inputStr: string): string {
374
- if (typeof inputStr === 'string') {
375
- return toCamelCase(inputStr).replace(/([A-Z])/g, '_$1').toLowerCase();
376
- }
377
- return inputStr;
378
- }
379
-
380
- /**
381
- * Takes an input array and makes sure the array has unique values by removing duplicates
382
- * @param array input with possible duplicates
383
- * @return array output without duplicates
384
- */
385
- export function uniqueArray<T = any>(arr: T[]): T[] {
386
- if (Array.isArray(arr) && arr.length > 0) {
387
- return arr.filter((item: T, index: number) => {
388
- return arr.indexOf(item) >= index;
389
- });
390
- }
391
- return arr;
392
- }
393
-
394
- /**
395
- * Takes an input array of objects and makes sure the array has unique object values by removing duplicates
396
- * it will loop through the array using a property name (or "id" when is not provided) to compare uniqueness
397
- * @param array input with possible duplicates
398
- * @param propertyName defaults to "id"
399
- * @return array output without duplicates
400
- */
401
- export function uniqueObjectArray(arr: any[], propertyName = 'id'): any[] {
402
- if (Array.isArray(arr) && arr.length > 0) {
403
- const result = [];
404
- const map = new Map();
405
-
406
- for (const item of arr) {
407
- if (item && !map.has(item[propertyName])) {
408
- map.set(item[propertyName], true); // set any value to Map
409
- result.push({
410
- id: item[propertyName],
411
- name: item.name
412
- });
413
- }
414
- }
415
- return result;
416
- }
417
- return arr;
418
- }
1
+ import type { AnyFunction } from './models/types';
2
+ import { extend } from './nodeExtend';
3
+
4
+ /**
5
+ * Add an item to an array only when the item does not exists, when the item is an object we will be using their "id" to compare
6
+ * @param inputArray
7
+ * @param inputItem
8
+ * @param itemIdPropName
9
+ */
10
+ export function addToArrayWhenNotExists<T = any>(inputArray: T[], inputItem: T, itemIdPropName = 'id') {
11
+ let arrayRowIndex = -1;
12
+ if (inputItem && typeof inputItem === 'object' && itemIdPropName in inputItem) {
13
+ arrayRowIndex = inputArray.findIndex((item) => item[itemIdPropName as keyof T] === inputItem[itemIdPropName as keyof T]);
14
+ } else {
15
+ arrayRowIndex = inputArray.findIndex((item) => item === inputItem);
16
+ }
17
+
18
+ if (arrayRowIndex < 0) {
19
+ inputArray.push(inputItem);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Simple function that will return a string with the number of whitespaces asked, mostly used by the CSV export
25
+ * @param {Number} nbSpaces - number of white spaces to create
26
+ * @param {String} spaceChar - optionally provide a different character to use for the whitespace (e.g. could be override to use "&nbsp;" in html)
27
+ */
28
+ export function addWhiteSpaces(nbSpaces: number, spaceChar = ' '): string {
29
+ let result = '';
30
+
31
+ for (let i = 0; i < nbSpaces; i++) {
32
+ result += spaceChar;
33
+ }
34
+ return result;
35
+ }
36
+
37
+ /**
38
+ * Remove an item from the array by its index
39
+ * @param array input
40
+ * @param index
41
+ */
42
+ export function arrayRemoveItemByIndex<T>(array: T[], index: number): T[] {
43
+ return array.filter((_el: T, i: number) => index !== i);
44
+ }
45
+
46
+ /**
47
+ * @use `extend()` when possible.
48
+ * Create an immutable clone of an array or object (native type will be returned "as is").
49
+ * This function will deep copy whatever is provided as the first argument, but nonetheless it is now an alias to `extend()` (node-extend) function.
50
+ * We'll keep the method to avoid breaking users of `deepCopy()`, however we suggest to directly use `extend(true, {}, obj)` whenever possible
51
+ */
52
+ export function deepCopy(objectOrArray: any | any[]): any | any[] {
53
+ // we previously had a full implementation but we can now use node-extend to do the job
54
+ // except that we need couple of small priors checks (native will be returned as is)
55
+ if (!Array.isArray(objectOrArray) && !isObject(objectOrArray)) {
56
+ return objectOrArray;
57
+ }
58
+ // Array or Object need 2nd arg to be either [] or {} for a deep copy to assign to when using node-extend (same as jQuery.extend())
59
+ const assignment = Array.isArray(objectOrArray) ? [] : {};
60
+ return extend(true, assignment, objectOrArray);
61
+ }
62
+
63
+ /**
64
+ * Performs a deep merge of objects and returns new object, it does not modify the source object, objects (immutable) and merges arrays via concatenation.
65
+ * Also, if first argument is undefined/null but next argument is an object then it will proceed and output will be an object
66
+ * @param {Object} target - the target object — what to apply the sources' properties to, which is returned after it is modified.
67
+ * @param {Object} sources - the source object(s) — objects containing the properties you want to apply.
68
+ * @returns {Object} The target object.
69
+ */
70
+ export function deepMerge(target: any, ...sources: any[]): any {
71
+ if (!sources.length) {
72
+ return target;
73
+ }
74
+ const source = sources.shift();
75
+
76
+ // when target is not an object but source is an object, then we'll assign as object
77
+ target = (!isObject(target) && isObject(source)) ? {} : target;
78
+
79
+ if (isObject(target) && isObject(source)) {
80
+ Object.keys(source).forEach(prop => {
81
+ if (source.hasOwnProperty(prop)) {
82
+ if (prop in target) {
83
+ // handling merging of two properties with equal names
84
+ if (typeof target[prop] !== 'object') {
85
+ target[prop] = source[prop];
86
+ } else {
87
+ if (typeof source[prop] !== 'object') {
88
+ target[prop] = source[prop];
89
+ } else {
90
+ if (target[prop].concat && source[prop].concat) {
91
+ // two arrays get concatenated
92
+ target[prop] = target[prop].concat(source[prop]);
93
+ } else {
94
+ // two objects get merged recursively
95
+ target[prop] = deepMerge(target[prop], source[prop]);
96
+ }
97
+ }
98
+ }
99
+ } else {
100
+ // new properties get added to target
101
+ target[prop] = source[prop];
102
+ }
103
+ }
104
+ });
105
+ }
106
+ return deepMerge(target, ...sources);
107
+ }
108
+
109
+ /**
110
+ * Empty an object properties by looping through them all and deleting them
111
+ * @param obj - input object
112
+ */
113
+ export function emptyObject(obj: any) {
114
+ if (isObject(obj)) {
115
+ Object.keys(obj).forEach(key => {
116
+ if (obj.hasOwnProperty(key)) {
117
+ delete obj[key];
118
+ }
119
+ });
120
+ }
121
+ obj = null;
122
+ obj = {};
123
+
124
+ return obj;
125
+ }
126
+
127
+ /**
128
+ * Get the function details (param & body) of a function.
129
+ * It supports regular function and also ES6 arrow functions
130
+ * @param {Function} fn - function to analyze
131
+ * @param {Boolean} [addReturn] - when using ES6 function as single liner, we could add the missing `return ...`
132
+ * @returns
133
+ */
134
+ export function getFunctionDetails(fn: AnyFunction, addReturn = true) {
135
+ let isAsyncFn = false;
136
+
137
+ const getFunctionBody = (func: AnyFunction) => {
138
+ const fnStr = func.toString();
139
+ isAsyncFn = fnStr.includes('async ');
140
+
141
+ // when fn is one liner arrow fn returning an object in brackets e.g. `() => ({ hello: 'world' })`
142
+ if ((fnStr.replaceAll(' ', '').includes('=>({'))) {
143
+ const matches = fnStr.match(/(({.*}))/g) || [];
144
+ return matches.length >= 1 ? `return ${matches[0]!.trimStart()}` : fnStr;
145
+ }
146
+ const isOneLinerArrowFn = (!fnStr.includes('{') && fnStr.includes('=>'));
147
+ const body = fnStr.substring(
148
+ (fnStr.indexOf('{') + 1) || (fnStr.indexOf('=>') + 2),
149
+ fnStr.includes('}') ? fnStr.lastIndexOf('}') : fnStr.length
150
+ );
151
+ if (addReturn && isOneLinerArrowFn && !body.startsWith('return')) {
152
+ return 'return ' + body.trimStart(); // add the `return ...` to the body for ES6 arrow fn
153
+ }
154
+ return body;
155
+ };
156
+
157
+ const getFunctionParams = (func: AnyFunction): string[] => {
158
+ const STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,)]*))/mg;
159
+ const ARG_NAMES = /([^\s,]+)/g;
160
+ const fnStr = func.toString().replace(STRIP_COMMENTS, '');
161
+ return fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARG_NAMES) ?? [];
162
+ };
163
+
164
+ return {
165
+ params: getFunctionParams(fn),
166
+ body: getFunctionBody(fn),
167
+ isAsync: isAsyncFn,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Check if an object is empty
173
+ * @param obj - input object
174
+ * @returns - boolean
175
+ */
176
+ export function isEmptyObject(obj: any): boolean {
177
+ if (obj === null || obj === undefined) {
178
+ return true;
179
+ }
180
+ return Object.entries(obj).length === 0;
181
+ }
182
+
183
+ export function isDefined<T>(value: T | undefined | null): value is T {
184
+ return <T>value !== undefined && <T>value !== null && <T>value !== '';
185
+ }
186
+
187
+ export function isDefinedNumber<T>(value: T | undefined | null): value is T {
188
+ return <T>value !== null && !isNaN(value as any) && <T>value !== '';
189
+ }
190
+
191
+ /**
192
+ * Simple object check.
193
+ * @param item
194
+ * @returns {boolean}
195
+ */
196
+ export function isObject(item: any) {
197
+ return item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date);
198
+ }
199
+
200
+ /**
201
+ * Simple check to detect if the value is a primitive type
202
+ * @param val
203
+ * @returns {boolean}
204
+ */
205
+ export function isPrimitiveValue(val: any) {
206
+ return typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string' || val === null || val === undefined;
207
+ }
208
+
209
+ export function isPrimitiveOrHTML(val: any) {
210
+ return val instanceof HTMLElement || val instanceof DocumentFragment || isPrimitiveValue(val);
211
+ }
212
+
213
+ /**
214
+ * Check if a value has any data (undefined, null or empty string will return False...)
215
+ * NOTE: a `false` boolean is consider as having data so it will return True
216
+ */
217
+ export function hasData(value: any): boolean {
218
+ return value !== undefined && value !== null && value !== '';
219
+ }
220
+
221
+ /**
222
+ * Check if input value is a number, by default it won't be a strict checking
223
+ * but optionally we could check for strict equality, for example "3" in strict mode would return False but True when non-strict.
224
+ * @param value - input value of any type
225
+ * @param strict - when using strict it also check for strict equality, e.g "3" in strict mode would return False but True when non-strict
226
+ */
227
+ export function isNumber(value: any, strict = false) {
228
+ if (strict) {
229
+ return (value === null || value === undefined || typeof value === 'string') ? false : !isNaN(value);
230
+ }
231
+ return (value === null || value === undefined || value === '') ? false : !isNaN(+value);
232
+ }
233
+
234
+ /** Check if an object is empty, it will also be considered empty when the input is null, undefined or isn't an object */
235
+ export function isObjectEmpty(obj: unknown) {
236
+ return !obj || (obj && typeof obj === 'object' && Object.keys(obj).length === 0);
237
+ }
238
+
239
+ /** Parse any input (bool, number, string) and return a boolean or False when not possible */
240
+ export function parseBoolean(input: any): boolean {
241
+ return /(true|1)/i.test(input + '');
242
+ }
243
+
244
+ /**
245
+ * Remove any accents from a string by normalizing it
246
+ * @param {String} text - input text
247
+ * @param {Boolean} shouldLowerCase - should we also lowercase the string output?
248
+ * @returns
249
+ */
250
+ export function removeAccentFromText(text: string, shouldLowerCase = false) {
251
+ const normalizedText = (typeof text.normalize === 'function') ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
252
+ return shouldLowerCase ? normalizedText.toLowerCase() : normalizedText;
253
+ }
254
+
255
+ /** Set the object value of deeper node from a given dot (.) notation path (e.g.: "user.firstName") */
256
+ export function setDeepValue<T = unknown>(obj: T, path: string | string[], value: any) {
257
+ if (typeof path === 'string') {
258
+ path = path.split('.');
259
+ }
260
+
261
+ if (path.length > 1) {
262
+ const e = path.shift() as keyof T;
263
+ if (obj && e !== undefined) {
264
+ setDeepValue(
265
+ (obj)[e] = (hasData(obj[e]) && (Array.isArray(obj[e]) || Object.prototype.toString.call((obj)[e]) === '[object Object]'))
266
+ ? (obj)[e]
267
+ : {} as any,
268
+ path,
269
+ value
270
+ );
271
+ }
272
+ } else if (obj && path[0]) {
273
+ (obj)[path[0] as keyof T] = value;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Title case (or capitalize) first char of a string, for example "hello world" will become "Hello world"
279
+ * Change the string to be title case on the complete sentence (upper case first char of each word while changing everything else to lower case)
280
+ * @param inputStr
281
+ * @returns string
282
+ */
283
+ export function titleCase(inputStr: string, shouldTitleCaseEveryWords = false): string {
284
+ if (typeof inputStr === 'string') {
285
+ if (shouldTitleCaseEveryWords) {
286
+ return inputStr.replace(/\w\S*/g, (outputStr) => {
287
+ return outputStr.charAt(0).toUpperCase() + outputStr.substring(1).toLowerCase();
288
+ });
289
+ }
290
+ return inputStr.charAt(0).toUpperCase() + inputStr.slice(1);
291
+ }
292
+ return inputStr;
293
+ }
294
+
295
+ /**
296
+ * Converts a string to camel case (camelCase), for example "hello-world" (or "hellow world") will become "helloWorld"
297
+ * @param inputStr the string to convert
298
+ * @return the string in camel case
299
+ */
300
+ export function toCamelCase(inputStr: string): string {
301
+ if (typeof inputStr === 'string') {
302
+ return inputStr.replace(/(?:^\w|[A-Z]|\b\w|[\s+\-_/])/g, (match: string, offset: number) => {
303
+ // remove white space or hypens or underscores
304
+ if (/[\s+\-_/]/.test(match)) {
305
+ return '';
306
+ }
307
+
308
+ return offset === 0 ? match.toLowerCase() : match.toUpperCase();
309
+ });
310
+ }
311
+ return inputStr;
312
+ }
313
+
314
+ /**
315
+ * Converts a string to kebab (hypen) case, for example "helloWorld" will become "hello-world"
316
+ * @param str the string to convert
317
+ * @return the string in kebab case
318
+ */
319
+ export function toKebabCase(inputStr: string): string {
320
+ if (typeof inputStr === 'string') {
321
+ return toCamelCase(inputStr).replace(/([A-Z])/g, '-$1').toLowerCase();
322
+ }
323
+ return inputStr;
324
+ }
325
+
326
+ /**
327
+ * Converts a camelCase or kebab-case string to a sentence case, for example "helloWorld" will become "Hello World" and "hello-world" will become "Hello world"
328
+ * @param str the string to convert
329
+ * @return the string in kebab case
330
+ */
331
+ export function toSentenceCase(inputStr: string): string {
332
+ if (typeof inputStr === 'string') {
333
+ const result = inputStr.replace(/([A-Z])|(-)/g, ' $1').replace(/\s+/g, ' ').trim();
334
+ return result.charAt(0).toUpperCase() + result.slice(1);
335
+ }
336
+ return inputStr;
337
+ }
338
+
339
+ /**
340
+ * Converts a string from camelCase to snake_case (underscore) case
341
+ * @param str the string to convert
342
+ * @return the string in kebab case
343
+ */
344
+ export function toSnakeCase(inputStr: string): string {
345
+ if (typeof inputStr === 'string') {
346
+ return toCamelCase(inputStr).replace(/([A-Z])/g, '_$1').toLowerCase();
347
+ }
348
+ return inputStr;
349
+ }
350
+
351
+ /**
352
+ * Takes an input array and makes sure the array has unique values by removing duplicates
353
+ * @param array input with possible duplicates
354
+ * @return array output without duplicates
355
+ */
356
+ export function uniqueArray<T = any>(arr: T[]): T[] {
357
+ if (Array.isArray(arr) && arr.length > 0) {
358
+ return arr.filter((item: T, index: number) => {
359
+ return arr.indexOf(item) >= index;
360
+ });
361
+ }
362
+ return arr;
363
+ }
364
+
365
+ /**
366
+ * Takes an input array of objects and makes sure the array has unique object values by removing duplicates
367
+ * it will loop through the array using a property name (or "id" when is not provided) to compare uniqueness
368
+ * @param array input with possible duplicates
369
+ * @param propertyName defaults to "id"
370
+ * @return array output without duplicates
371
+ */
372
+ export function uniqueObjectArray(arr: any[], propertyName = 'id'): any[] {
373
+ if (Array.isArray(arr) && arr.length > 0) {
374
+ const result = [];
375
+ const map = new Map();
376
+
377
+ for (const item of arr) {
378
+ if (item && !map.has(item[propertyName])) {
379
+ map.set(item[propertyName], true); // set any value to Map
380
+ result.push({
381
+ id: item[propertyName],
382
+ name: item.name
383
+ });
384
+ }
385
+ }
386
+ return result;
387
+ }
388
+ return arr;
389
+ }