shineout
Version:
Shein 前端组件库
223 lines (187 loc) • 6.66 kB
text/typescript
import { isObject, isMergeable } from './is'
import { insertPoint } from './flat'
import { ObjectType } from '../@types/common'
const { hasOwnProperty } = Object.prototype
const PATH_MODE = {
loose: '?',
strict: '!',
insert: '^',
append: '$',
}
interface deepOptions {
clone?: boolean
forceSet?: boolean
strictMode?: boolean
defaultValue?: any
skipUndefined?: boolean
removeUndefined?: boolean
}
export function filterProps<T extends ObjectType>(obj: T, props: (keyof T)[] | ((prop: any) => boolean) = []) {
if (!isObject(obj)) return obj
if (typeof props === 'function') {
const prediction = props
props = []
Object.keys(obj).forEach(k => {
if (prediction(obj[k as keyof T])) (props as (keyof T)[]).push(k as keyof T)
})
}
const newObj: Partial<T> = {}
props.forEach(k => {
newObj[k] = obj[k]
})
return newObj
}
// Object.values()
export const objectValues = (obj: ObjectType) => {
if (!obj) return []
return Object.keys(obj).map(k => obj[k])
}
// object only, not handle array.
export const deepMerge = (target: ObjectType = {}, source: ObjectType, options: deepOptions = {}) => {
const { clone, removeUndefined, skipUndefined } = options
if (!isMergeable(source)) return source
const dest: ObjectType = {}
if (isMergeable(target)) {
Object.keys(target).forEach(k => {
dest[k] = clone ? deepMerge({}, target[k], options) : target[k]
if (removeUndefined && dest[k] === undefined) delete dest[k]
})
}
Object.keys(source).forEach(k => {
if (isMergeable(source[k]) && isMergeable(target[k])) {
dest[k] = deepMerge(target[k], source[k], options)
} else {
if (skipUndefined && source[k] === undefined) return
dest[k] = deepMerge({}, source[k], options)
if (removeUndefined && dest[k] === undefined) delete dest[k]
}
})
return dest
}
export function pathGenerator(raw: string) {
const path = insertPoint(raw)
const reg = /^\[(\d+)\]$/
const pathModeValues = objectValues(PATH_MODE)
let index = 0
let last = 0
let prop: string | number = ''
const results: Array<[string | number, string | undefined, string | undefined]> = []
while (index >= 0) {
index = path.indexOf('.', last)
prop = path.substring(last, index === -1 ? undefined : index)
let mode
const lastChar = prop.charAt(prop.length - 1)
if (pathModeValues.includes(lastChar)) {
mode = lastChar
prop = prop.substring(0, prop.length - 1)
}
// array index
const match = reg.exec(prop)
// eslint-disable-next-line
if (match) prop = parseInt(match[1], 10)
last = index + 1
results.push([prop, index === -1 ? undefined : path.substring(last), mode])
}
return results
}
export const deepSet = (target: ObjectType, path: string, value: any, options: deepOptions = {}) => {
if (!isObject(target)) throw new Error('Target must be an object.')
if (typeof path !== 'string') throw new Error('Path must be a string.')
const { removeUndefined, skipUndefined } = options
const mergeOptions = { clone: true, removeUndefined, skipUndefined }
// empty root
if (path === '') {
const dest = deepMerge(target, value, mergeOptions)
Object.keys(dest).forEach(k => {
;((target as unknown) as ObjectType)[k] = dest[k]
})
return target
}
let current: any = target
for (const [prop, next, mode] of pathGenerator(path)) {
if (next) {
const nextIsArray = /^\[\d+\]/.test(next as string)
if (!current[prop]) current[prop] = nextIsArray ? [] : {}
if (nextIsArray && !Array.isArray(current[prop])) {
throw new Error(`Path ${path} expect an array.`)
} else if (Array.isArray(current[prop]) && !nextIsArray) {
throw new Error(`Path ${path} is an array, expect an object.`)
}
current = current[prop]
continue
}
if (options.forceSet) {
current[prop] = value
} else if (mode === PATH_MODE.insert) {
current.splice(prop, 0, value)
} else if (mode === PATH_MODE.append) {
current.splice((prop as number) + 1, 0, value)
} else {
if (skipUndefined && value === undefined) break
current[prop] =
isMergeable(current[prop]) && isMergeable(value) ? deepMerge(current[prop], value, mergeOptions) : value
}
if (removeUndefined && value === undefined) delete current[prop]
}
return target
}
export const deepGet = (target: ObjectType, path: string, options: deepOptions = {}) => {
if (!isObject(target)) throw new Error('Target must be an object.')
if (typeof path !== 'string') throw new Error('Path must be a string.')
// empty root
if (path === '') return target
const { defaultValue, strictMode } = options
let current: unknown = target
for (const [prop, , mode] of pathGenerator(path)) {
const isStrict = mode === PATH_MODE.strict || (strictMode && defaultValue === undefined && mode !== PATH_MODE.loose)
if (current != null && hasOwnProperty.call(current, prop)) {
current = (current as ObjectType)[prop]
} else if (isStrict) {
throw new Error(`Path ${path} is not exist.`)
} else {
current = defaultValue
break
}
}
return current
}
export const deepRemove = (target: ObjectType, path: string) => {
if (!isObject(target)) throw new Error('Target must be an object.')
if (typeof path !== 'string' || !path) throw new Error('Path must be a string.')
let current: ObjectType = target
let nextIsArray = false
for (const [prop, next] of pathGenerator(path)) {
if (current == null || !hasOwnProperty.call(current, prop)) {
break
}
if (next) {
current = current[prop as string]
nextIsArray = /^\[\d+\]/.test(next)
} else if (isObject(current)) {
if (nextIsArray) throw new Error('Target is an object, expect array')
delete (current as ObjectType)[prop]
} else {
if (!nextIsArray) {
throw new Error('Target is an array, expect object')
}
;(current as any[]).splice(prop as number, 1)
}
}
return target
}
export const deepHas = (target: ObjectType, path: string) => {
if (!isObject(target)) throw new Error('Target must be an object.')
if (typeof path !== 'string') throw new Error('Path must be a string.')
if (path === '') return true
let current: any = target
for (const [prop, ,] of pathGenerator(path)) {
if (!current || !hasOwnProperty.call(current, prop)) return false
current = current[prop]
}
return true
}
export const entries = (obj: ObjectType) => {
if (!obj) return []
const keys = Object.keys(obj)
return keys.map(key => [key, obj[key]])
}