@splendidlabz/utils 1.3.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/.eslintrc.cjs +3 -0
- package/.turbo/turbo-lint.log +10 -0
- package/.turbo/turbo-test.log +10 -0
- package/CHANGELOG.md +37 -0
- package/actions/index.js +5 -0
- package/actions/masonry.js +38 -0
- package/actions/mutation-observer.js +43 -0
- package/actions/prefer-horizontal-scroll.js +125 -0
- package/actions/resize-observer.js +38 -0
- package/actions/sticky.js +88 -0
- package/dom/bounding-box.js +54 -0
- package/dom/clipboard.js +14 -0
- package/dom/cookie.js +31 -0
- package/dom/css-vars.js +11 -0
- package/dom/events.js +76 -0
- package/dom/events.test.js +99 -0
- package/dom/focusable.js +56 -0
- package/dom/font-size.js +24 -0
- package/dom/get-element.js +69 -0
- package/dom/hash.js +9 -0
- package/dom/index.js +18 -0
- package/dom/keyboard.js +39 -0
- package/dom/local-store.js +29 -0
- package/dom/media.js +27 -0
- package/dom/pkce.js +34 -0
- package/dom/query-params.js +14 -0
- package/dom/random-string.js +18 -0
- package/dom/session-store.js +29 -0
- package/dom/trap-focus.js +55 -0
- package/dom/ui/aria-current.js +20 -0
- package/dom/ui/inconsistent-button-fix.js +8 -0
- package/dom/ui/index.js +4 -0
- package/dom/ui/scroll-container.js +25 -0
- package/dom/ui/traverse-and-scramble.js +40 -0
- package/lib/arrays/index.js +62 -0
- package/lib/auth/index.js +1 -0
- package/lib/auth/route-manager.js +44 -0
- package/lib/checks.js +11 -0
- package/lib/date/days.js +9 -0
- package/lib/date/index.js +2 -0
- package/lib/date/months.js +74 -0
- package/lib/form/form-data.js +73 -0
- package/lib/form/index.js +2 -0
- package/lib/form/sanitize.js +47 -0
- package/lib/functions/debounce.js +18 -0
- package/lib/functions/env.js +5 -0
- package/lib/functions/functional.js +31 -0
- package/lib/functions/index.js +5 -0
- package/lib/functions/throttle.js +11 -0
- package/lib/functions/timeout.js +15 -0
- package/lib/index.js +12 -0
- package/lib/index.test.js +9 -0
- package/lib/numbers/index.js +10 -0
- package/lib/objects/camelcase-keys.js +17 -0
- package/lib/objects/camelcase-keys.test.js +72 -0
- package/lib/objects/empty.js +10 -0
- package/lib/objects/extend.js +16 -0
- package/lib/objects/extend.test.js +35 -0
- package/lib/objects/flatten.js +19 -0
- package/lib/objects/index.js +12 -0
- package/lib/objects/json.js +7 -0
- package/lib/objects/loop.js +11 -0
- package/lib/objects/loop.test.js +31 -0
- package/lib/objects/mix/mix.js +100 -0
- package/lib/objects/mix/mix.md +78 -0
- package/lib/objects/mix/mix.test.js +324 -0
- package/lib/objects/nested-property.js +36 -0
- package/lib/objects/normalize-object.js +10 -0
- package/lib/objects/omit-empty.js +23 -0
- package/lib/objects/omit-empty.test.js +85 -0
- package/lib/objects/size.js +41 -0
- package/lib/objects/split.js +19 -0
- package/lib/promises/index.js +1 -0
- package/lib/promises/reject.js +17 -0
- package/lib/strings/convert-case/convert-case.js +86 -0
- package/lib/strings/convert-case/convert-case.md +44 -0
- package/lib/strings/convert-case/convert-case.test.js +50 -0
- package/lib/strings/index.js +4 -0
- package/lib/strings/markdown.js +39 -0
- package/lib/strings/pluralize.js +2 -0
- package/lib/strings/query-string.js +18 -0
- package/lib/style/index.js +11 -0
- package/lib/symbols/index.js +1 -0
- package/lib/symbols/symbols.js +9 -0
- package/node/common.js +5 -0
- package/node/dirname.js +17 -0
- package/node/file-cache.js +135 -0
- package/node/file.js +13 -0
- package/node/hash.js +5 -0
- package/node/index.js +6 -0
- package/node/pkce.js +33 -0
- package/node/random-string.js +10 -0
- package/package.json +35 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const MONTHS = [
|
|
2
|
+
{
|
|
3
|
+
long: 'January',
|
|
4
|
+
short: 'jan',
|
|
5
|
+
numeric: 1,
|
|
6
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
long: 'February',
|
|
10
|
+
short: 'feb',
|
|
11
|
+
numeric: 2,
|
|
12
|
+
lastDay: { default: 28, leapYear: 29 },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
long: 'March',
|
|
16
|
+
short: 'mar',
|
|
17
|
+
numeric: 3,
|
|
18
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
long: 'April',
|
|
22
|
+
short: 'apr',
|
|
23
|
+
numeric: 4,
|
|
24
|
+
lastDay: { default: 30, leapYear: 30 },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
long: 'May',
|
|
28
|
+
short: 'may',
|
|
29
|
+
numeric: 5,
|
|
30
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
long: 'June',
|
|
34
|
+
short: 'jun',
|
|
35
|
+
numeric: 6,
|
|
36
|
+
lastDay: { default: 30, leapYear: 30 },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
long: 'July',
|
|
40
|
+
short: 'jul',
|
|
41
|
+
numeric: 7,
|
|
42
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
long: 'August',
|
|
46
|
+
short: 'aug',
|
|
47
|
+
numeric: 8,
|
|
48
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
long: 'September',
|
|
52
|
+
short: 'sep',
|
|
53
|
+
numeric: 9,
|
|
54
|
+
lastDay: { default: 30, leapYear: 30 },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
long: 'October',
|
|
58
|
+
short: 'oct',
|
|
59
|
+
numeric: 10,
|
|
60
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
long: 'November',
|
|
64
|
+
short: 'nov',
|
|
65
|
+
numeric: 11,
|
|
66
|
+
lastDay: { default: 30, leapYear: 30 },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
long: 'December',
|
|
70
|
+
short: 'dec',
|
|
71
|
+
numeric: 12,
|
|
72
|
+
lastDay: { default: 31, leapYear: 31 },
|
|
73
|
+
},
|
|
74
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Form Data into an object
|
|
3
|
+
* @param {FormData} formData
|
|
4
|
+
* @returns Object
|
|
5
|
+
*/
|
|
6
|
+
export function formDataToObject(formData) {
|
|
7
|
+
const obj = {}
|
|
8
|
+
for (let [key, value] of formData) {
|
|
9
|
+
if (typeof value === 'string') value = value.trim()
|
|
10
|
+
|
|
11
|
+
if (key.includes('[]')) {
|
|
12
|
+
// Handle array fields
|
|
13
|
+
const arrayKey = key.replace('[]', '')
|
|
14
|
+
if (!obj[arrayKey]) obj[arrayKey] = []
|
|
15
|
+
obj[arrayKey].push(value)
|
|
16
|
+
} else {
|
|
17
|
+
// Handle normal string fields
|
|
18
|
+
obj[key] = value
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return obj
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function flattenArrayFields(inputObject) {
|
|
25
|
+
const result = {}
|
|
26
|
+
|
|
27
|
+
for (const [key, value] of Object.entries(inputObject)) {
|
|
28
|
+
// Regex to match keys that follow pattern: arrayName[index][property]
|
|
29
|
+
const match = key.match(/^(\w+)(?:\[(\d+)\])?(?:\[(\w+)\])?$/)
|
|
30
|
+
if (match) {
|
|
31
|
+
const [, baseKey, index, property] = match
|
|
32
|
+
|
|
33
|
+
if (index !== undefined) {
|
|
34
|
+
// Handle array of objects
|
|
35
|
+
if (!result[baseKey]) result[baseKey] = []
|
|
36
|
+
const arrayIndex = parseInt(index)
|
|
37
|
+
if (!result[baseKey][arrayIndex]) result[baseKey][arrayIndex] = {}
|
|
38
|
+
if (property) {
|
|
39
|
+
result[baseKey][arrayIndex][property] = value
|
|
40
|
+
} else {
|
|
41
|
+
result[baseKey][arrayIndex] = value
|
|
42
|
+
}
|
|
43
|
+
} else if (property) {
|
|
44
|
+
// Handle single object
|
|
45
|
+
if (!result[baseKey]) result[baseKey] = {}
|
|
46
|
+
result[baseKey][property] = value
|
|
47
|
+
} else {
|
|
48
|
+
// Handle simple key-value
|
|
49
|
+
result[key] = value
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Copy key-value pair for unmatched keys
|
|
53
|
+
result[key] = value
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Remove empty objects from arrays and convert array-like objects to actual arrays
|
|
58
|
+
for (const key in result) {
|
|
59
|
+
if (typeof result[key] === 'object' && result[key] !== null) {
|
|
60
|
+
if (Object.keys(result[key]).every(k => !isNaN(parseInt(k)))) {
|
|
61
|
+
// Convert to array and filter out empty items
|
|
62
|
+
result[key] = Object.values(result[key]).filter(item => {
|
|
63
|
+
if (typeof item === 'object') {
|
|
64
|
+
return Object.values(item).some(v => v !== '')
|
|
65
|
+
}
|
|
66
|
+
return item !== ''
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sanitizes HTML string using DOMPurify.
|
|
5
|
+
* @param {string} html - The HTML string to sanitize.
|
|
6
|
+
* @param {Object} [options={}] - DOMPurify configuration options.
|
|
7
|
+
* @returns {string} The sanitized HTML string.
|
|
8
|
+
*/
|
|
9
|
+
export function sanitize(html, options = {}) {
|
|
10
|
+
return DOMPurify.sanitize(html, options)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Recursively sanitizes entries in an object or array.
|
|
15
|
+
* @param {Array} entries - The entries to sanitize.
|
|
16
|
+
* @param {Object} [options={}] - DOMPurify configuration options.
|
|
17
|
+
* @returns {Array} The sanitized entries.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeEntries(entries, options = {}) {
|
|
20
|
+
return entries.map(([key, value]) => {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return [
|
|
23
|
+
key,
|
|
24
|
+
value.map(item => sanitizeEntries(Object.entries(item), options)),
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
return [
|
|
30
|
+
key,
|
|
31
|
+
Object.fromEntries(sanitizeEntries(Object.entries(value), options)),
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [key, sanitize(value, options)]
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sanitizes all string values in an object.
|
|
41
|
+
* @param {Object} obj - The object to sanitize.
|
|
42
|
+
* @param {Object} [options={}] - DOMPurify configuration options.
|
|
43
|
+
* @returns {Object} A new object with all string values sanitized.
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeObject(obj, options = {}) {
|
|
46
|
+
return Object.fromEntries(sanitizeEntries(Object.entries(obj), options))
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function debounce(callback, wait, immediate = false) {
|
|
2
|
+
let timeout
|
|
3
|
+
|
|
4
|
+
return function () {
|
|
5
|
+
const context = this
|
|
6
|
+
const args = arguments
|
|
7
|
+
|
|
8
|
+
const callNow = immediate && !timeout
|
|
9
|
+
clearTimeout(timeout)
|
|
10
|
+
timeout = setTimeout(later, wait)
|
|
11
|
+
if (callNow) callback.apply(context, args)
|
|
12
|
+
|
|
13
|
+
function later() {
|
|
14
|
+
timeout = null
|
|
15
|
+
if (!immediate) callback.apply(context, args)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function curry(fn) {
|
|
2
|
+
return function curried(...args) {
|
|
3
|
+
if (args.length >= fn.length) {
|
|
4
|
+
return fn.apply(this, args)
|
|
5
|
+
} else {
|
|
6
|
+
return function (...moreArgs) {
|
|
7
|
+
return curried.apply(this, args.concat(moreArgs))
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function compose(...fns) {
|
|
14
|
+
return function (value) {
|
|
15
|
+
return fns.reduceRight((acc, fn) => fn(acc), value)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pipe(...fns) {
|
|
20
|
+
return function (value) {
|
|
21
|
+
return fns.reduce((acc, fn) => fn(acc), value)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function times(fn, n) {
|
|
26
|
+
const result = []
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
result.push(fn(i))
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// setTimeout for task ticking
|
|
2
|
+
// reverses wait and callback functions of timeouts because it makes more sense when reading the code
|
|
3
|
+
export function timeout(wait, callback) {
|
|
4
|
+
setTimeout(callback, wait)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Promise based timeouts for microtask ticking
|
|
8
|
+
export function delay(ms) {
|
|
9
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Alias of delay
|
|
13
|
+
export function wait(ms) {
|
|
14
|
+
return delay(ms)
|
|
15
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './arrays/index.js'
|
|
2
|
+
export * from './auth/index.js'
|
|
3
|
+
export * from './checks.js'
|
|
4
|
+
export * from './date/index.js'
|
|
5
|
+
export * from './form/index.js'
|
|
6
|
+
export * from './functions/index.js'
|
|
7
|
+
export * from './numbers/index.js'
|
|
8
|
+
export * from './objects/index.js'
|
|
9
|
+
export * from './promises/index.js'
|
|
10
|
+
export * from './strings/index.js'
|
|
11
|
+
export * from './style/index.js'
|
|
12
|
+
export * from './symbols/index.js'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function splitUnit(string) {
|
|
2
|
+
// If there are no units, return the number straight.
|
|
3
|
+
if (typeof string === 'number') return [string, null]
|
|
4
|
+
|
|
5
|
+
const regex = /(-?\d*\.?\d+)([a-z]+)/i
|
|
6
|
+
const match = string.match(regex)
|
|
7
|
+
|
|
8
|
+
if (match) return [parseFloat(match[1]), match[2]]
|
|
9
|
+
return [parseFloat(string), null] // sometimes unitless numbers make it here if was typed into a string. This catches and returns it.
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { objectForEach } from './loop.js'
|
|
2
|
+
import { toCamel } from '../strings/convert-case/convert-case.js'
|
|
3
|
+
|
|
4
|
+
export function camelCaseKeys(obj) {
|
|
5
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
6
|
+
return obj
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const result = {}
|
|
10
|
+
|
|
11
|
+
objectForEach(obj, ({ key, value }) => {
|
|
12
|
+
const camelCaseKey = toCamel(key)
|
|
13
|
+
result[camelCaseKey] = camelCaseKeys(value)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { camelCaseKeys } from './camelcase-keys.js'
|
|
4
|
+
|
|
5
|
+
it('Camelcase Basic', ctx => {
|
|
6
|
+
const object = {
|
|
7
|
+
lowercase: 1,
|
|
8
|
+
'Title Case': 2,
|
|
9
|
+
'kebab-case': 3,
|
|
10
|
+
PascalCase: 4,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const expected = {
|
|
14
|
+
lowercase: 1,
|
|
15
|
+
titleCase: 2,
|
|
16
|
+
kebabCase: 3,
|
|
17
|
+
pascalCase: 4,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = camelCaseKeys(object)
|
|
21
|
+
expect(result).toEqual(expected)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('Camelcase Nested', ctx => {
|
|
25
|
+
const object = {
|
|
26
|
+
one: {
|
|
27
|
+
lowercase: 1,
|
|
28
|
+
'Title Case': 2,
|
|
29
|
+
'kebab-case': 3,
|
|
30
|
+
PascalCase: 4,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const expected = {
|
|
35
|
+
one: {
|
|
36
|
+
lowercase: 1,
|
|
37
|
+
titleCase: 2,
|
|
38
|
+
kebabCase: 3,
|
|
39
|
+
pascalCase: 4,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = camelCaseKeys(object)
|
|
44
|
+
expect(result).toEqual(expected)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('Camelcase Deeply Nested', ctx => {
|
|
48
|
+
const object = {
|
|
49
|
+
one: {
|
|
50
|
+
two: {
|
|
51
|
+
lowercase: 1,
|
|
52
|
+
'Title Case': 2,
|
|
53
|
+
'kebab-case': 3,
|
|
54
|
+
PascalCase: 4,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const expected = {
|
|
60
|
+
one: {
|
|
61
|
+
two: {
|
|
62
|
+
lowercase: 1,
|
|
63
|
+
titleCase: 2,
|
|
64
|
+
kebabCase: 3,
|
|
65
|
+
pascalCase: 4,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = camelCaseKeys(object)
|
|
71
|
+
expect(result).toEqual(expected)
|
|
72
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function isEmpty(obj) {
|
|
2
|
+
if (typeof obj === 'undefined') return true
|
|
3
|
+
if (obj === null) return true
|
|
4
|
+
if (Array.isArray(obj)) return obj.length === 0
|
|
5
|
+
return Object.keys(obj).length === 0
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function notEmpty(obj) {
|
|
9
|
+
return !isEmpty(obj)
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { objectForEach, objectMap } from './loop.js'
|
|
2
|
+
|
|
3
|
+
import { curry } from '../functions/functional.js'
|
|
4
|
+
import { mix } from './mix/mix.js'
|
|
5
|
+
|
|
6
|
+
// Need to manually curry these functions.
|
|
7
|
+
|
|
8
|
+
export function extendObject(object) {
|
|
9
|
+
const orig = object
|
|
10
|
+
const extended = mix({}, object)
|
|
11
|
+
|
|
12
|
+
extended.forEach = curry(objectForEach)(orig)
|
|
13
|
+
extended.map = curry(objectMap)(orig)
|
|
14
|
+
|
|
15
|
+
return { orig, extended }
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { beforeEach, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { extendObject } from '.'
|
|
4
|
+
|
|
5
|
+
beforeEach(ctx => {
|
|
6
|
+
ctx.object = {
|
|
7
|
+
one: 1,
|
|
8
|
+
two: 2,
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('Object Extension', ctx => {
|
|
13
|
+
const { object } = ctx
|
|
14
|
+
const { extended, orig } = extendObject(object)
|
|
15
|
+
|
|
16
|
+
// Orig should be untouched
|
|
17
|
+
// Object should also be untouched
|
|
18
|
+
expect(object).toEqual(orig)
|
|
19
|
+
expect(object.forEach).toBeUndefined()
|
|
20
|
+
expect(object.map).toBeUndefined()
|
|
21
|
+
|
|
22
|
+
// Extended should be extended
|
|
23
|
+
expect(extended.one).toBe(1)
|
|
24
|
+
expect(extended.two).toBe(2)
|
|
25
|
+
expect(extended.forEach).toBeDefined()
|
|
26
|
+
expect(extended.map).toBeDefined()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Curried methods', ctx => {
|
|
30
|
+
const { extended } = extendObject(ctx.object)
|
|
31
|
+
|
|
32
|
+
// Extended methods should not be part of the object when looping
|
|
33
|
+
const test = extended.map(({ key, value }) => value)
|
|
34
|
+
expect(test).toEqual([1, 2])
|
|
35
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isObject } from '../checks.js'
|
|
2
|
+
|
|
3
|
+
export function flattenObject(obj, { separator, prefix = '' }) {
|
|
4
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
5
|
+
const newKey = prefix ? `${prefix}${separator}${key}` : key
|
|
6
|
+
|
|
7
|
+
const isAnObject = isObject(value)
|
|
8
|
+
const hasNestedObject = isAnObject && Object.values(value).some(isObject)
|
|
9
|
+
|
|
10
|
+
if (hasNestedObject) {
|
|
11
|
+
// If the value is an object, recursively flatten it.
|
|
12
|
+
Object.assign(acc, flattenObject(value, { separator, prefix: newKey }))
|
|
13
|
+
} else {
|
|
14
|
+
// Directly assign terminal values to the new key in the accumulator.
|
|
15
|
+
acc[newKey] = value
|
|
16
|
+
}
|
|
17
|
+
return acc
|
|
18
|
+
}, {})
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './camelcase-keys.js'
|
|
2
|
+
export * from './empty.js'
|
|
3
|
+
export * from './extend.js'
|
|
4
|
+
export * from './flatten.js'
|
|
5
|
+
export * from './json.js'
|
|
6
|
+
export * from './loop.js'
|
|
7
|
+
export * from './mix/mix.js'
|
|
8
|
+
export * from './nested-property.js'
|
|
9
|
+
export * from './normalize-object.js'
|
|
10
|
+
export * from './omit-empty.js'
|
|
11
|
+
export * from './size.js'
|
|
12
|
+
export * from './split.js'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function objectForEach(object, callback) {
|
|
2
|
+
Object.entries(object).forEach(([key, value]) => {
|
|
3
|
+
callback({ key, value })
|
|
4
|
+
})
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function objectMap(object, callback) {
|
|
8
|
+
return Object.entries(object).map(([key, value]) => {
|
|
9
|
+
return callback({ key, value })
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { beforeEach, expect, it } from 'vitest'
|
|
2
|
+
import { objectForEach, objectMap } from './loop.js'
|
|
3
|
+
|
|
4
|
+
beforeEach(ctx => {
|
|
5
|
+
ctx.obj = {
|
|
6
|
+
one: 1,
|
|
7
|
+
two: 2,
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('Object For Each', ctx => {
|
|
12
|
+
const { obj } = ctx
|
|
13
|
+
const newObject = {}
|
|
14
|
+
function callback({ key, value }) {
|
|
15
|
+
newObject[key] = value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
objectForEach(obj, callback)
|
|
19
|
+
expect(newObject).toEqual(obj)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('Object Map', ctx => {
|
|
23
|
+
const { obj } = ctx
|
|
24
|
+
|
|
25
|
+
function callback({ key, value }) {
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = objectMap(obj, callback)
|
|
30
|
+
expect(res).toEqual([1, 2])
|
|
31
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export function mix(...sources) {
|
|
2
|
+
const result = {}
|
|
3
|
+
for (const source of sources) {
|
|
4
|
+
merge(result, source)
|
|
5
|
+
}
|
|
6
|
+
return result
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function merge(output, input) {
|
|
10
|
+
const props = Object.keys(input)
|
|
11
|
+
|
|
12
|
+
for (const prop of props) {
|
|
13
|
+
// Prevents Prototype Pollution
|
|
14
|
+
if (prop === '__proto__') continue
|
|
15
|
+
|
|
16
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, prop)
|
|
17
|
+
const value = descriptor.value
|
|
18
|
+
if (value) descriptor.value = cloneDescriptorValue(value)
|
|
19
|
+
|
|
20
|
+
// If don't have prop => Define property
|
|
21
|
+
if (!output[prop]) {
|
|
22
|
+
Object.defineProperty(output, prop, descriptor)
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If have prop, but type is not object => Overwrite by redefining property
|
|
27
|
+
if (typeof output[prop] !== 'object') {
|
|
28
|
+
Object.defineProperty(output, prop, descriptor)
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If have prop, but type is Object => Concat the arrays together.
|
|
33
|
+
if (objectType(descriptor.value) === '[object Array]') {
|
|
34
|
+
output[prop] = output[prop].concat(descriptor.value)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If have prop, but type is Object => Merge.
|
|
39
|
+
merge(output[prop], descriptor.value)
|
|
40
|
+
|
|
41
|
+
// Not sure if I have to deal with overwriting maps / dates. When it comes, I guess!
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Creates a deep clone for each value
|
|
46
|
+
function cloneDescriptorValue(value) {
|
|
47
|
+
// Arrays
|
|
48
|
+
if (objectType(value) === '[object Array]') {
|
|
49
|
+
const array = []
|
|
50
|
+
for (let v of value) {
|
|
51
|
+
v = cloneDescriptorValue(v)
|
|
52
|
+
array.push(v)
|
|
53
|
+
}
|
|
54
|
+
return array
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Objects
|
|
58
|
+
if (objectType(value) === '[object Object]') {
|
|
59
|
+
const obj = {}
|
|
60
|
+
const props = Object.keys(value)
|
|
61
|
+
for (const prop of props) {
|
|
62
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, prop)
|
|
63
|
+
if (descriptor.value)
|
|
64
|
+
descriptor.value = cloneDescriptorValue(descriptor.value)
|
|
65
|
+
Object.defineProperty(obj, prop, descriptor)
|
|
66
|
+
}
|
|
67
|
+
return obj
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Other Types of Objects
|
|
71
|
+
if (objectType(value) === '[object Date]') {
|
|
72
|
+
return new Date(value.getTime())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (objectType(value) === '[object Map]') {
|
|
76
|
+
const map = new Map()
|
|
77
|
+
for (const entry of value) {
|
|
78
|
+
map.set(entry[0], cloneDescriptorValue(entry[1]))
|
|
79
|
+
}
|
|
80
|
+
return map
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (objectType(value) === '[object Set]') {
|
|
84
|
+
const set = new Set()
|
|
85
|
+
for (const entry of value.entries()) {
|
|
86
|
+
set.add(cloneDescriptorValue(entry[0]))
|
|
87
|
+
}
|
|
88
|
+
return set
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Types we don't need to clone or cannot clone.
|
|
92
|
+
// Examples:
|
|
93
|
+
// - Primitives don't need to clone
|
|
94
|
+
// - Functions cannot clone
|
|
95
|
+
return value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function objectType(value) {
|
|
99
|
+
return Object.prototype.toString.call(value)
|
|
100
|
+
}
|