@bigfishtv/cockpit
Version:
492 lines (452 loc) • 14.6 kB
JavaScript
/**
* Tree Utilities
* @module Utilities/treeUtils
*/
import Immutable from 'immutable'
import cloneDeep from 'lodash/cloneDeep'
//// Immutable.js functions ////
/**
* Recursively iterates through tree looking for id
* @param {Number} id
* @param {Immutable.List} Branch
* @param {String} [iteratorKey=children]
* @return {Immutable.Map}
*/
export function getChildByIdImmutable(id, Branch, iteratorKey = 'children') {
if (
typeof id !== 'number' ||
typeof Branch !== 'object' ||
typeof iteratorKey !== 'string' ||
!Immutable.List.isList(Branch)
)
return false
let found = false
Branch.map(item => {
if (!found) {
if (item.get('id') === id) found = item
else if (item.get(iteratorKey)) found = getChildByIdImmutable(id, item.get(iteratorKey))
}
})
return found
}
/**
* @deprecated This function is super-inefficient and should not be used.
*
* Recursively iterates through tree looking for parent of child's id
* @param {Number} id
* @param {Immutable.List} Branch
* @param {String} [iteratorKey=children]
* @return {Immutable.Map}
*/
export function getParentByChildIdImmutable(id, Branch, iteratorKey = 'children') {
if (
typeof id !== 'number' ||
typeof Branch !== 'object' ||
typeof iteratorKey !== 'string' ||
!Immutable.List.isList(Branch)
)
return false
let found = false
Branch.map(item => {
if (!found) {
if (item.get(iteratorKey))
item.get(iteratorKey).map(child => {
if (!found) {
if (child.get('id') === id) {
found = item
} else found = getParentByChildIdImmutable(id, item.get(iteratorKey))
}
})
}
})
return found
}
/**
* Recursively iterates through tree using a checker function to determine whether or not to add a branch's key to an array of values
* @param {Immutable.List} tree
* @param {String} key
* @param {Function} checker
* @param {String} [iteratorKey=children]
* @return {Array} - returns array of values
*/
export function collectValuesImmutable(tree, key, checker, iteratorKey = 'children') {
if (
typeof tree !== 'object' ||
!Immutable.List.isList(tree) ||
typeof key !== 'string' ||
typeof checker !== 'function' ||
typeof iteratorKey !== 'string'
)
return false
let values = []
tree.map(branch => {
if (checker(branch) && branch.get(key)) values.push(branch.get(key))
if (branch.get(iteratorKey) && branch.get(iteratorKey).size > 0)
values = values.concat(collectValuesImmutable(branch.get(iteratorKey), key, checker, iteratorKey))
})
return values
}
/**
* Prunes a tree's branches based on a key and an array of disallowed values
* @param {Immutable.List} tree
* @param {String} key
* @param {Array} values - array of key values that determine if a branch is to be pruned
* @param {String} [iteratorKey=children]
* @return {Immutable.List} - returns pruned tree
*/
export function pruneTreeImmutable(tree, key, values, iteratorKey = 'children') {
if (typeof tree !== 'object' || !Immutable.List.isList(tree) || typeof iteratorKey !== 'string') return false
if (!(values instanceof Array)) values = [values]
tree = tree.filter(branch => values.indexOf(branch.get(key)) < 0)
tree = tree.map(branch => {
if (branch.get(iteratorKey) && branch.get(iteratorKey).size > 0) {
return branch.set(iteratorKey, pruneTreeImmutable(branch.get(iteratorKey), key, values, iteratorKey))
}
return branch
})
return tree
}
//// Regular array/object functions ////
/**
* Recursively iterates through tree looking for id
* @param {Number} id
* @param {Object[]} Branch
* @param {String} [iteratorKey=children]
* @return {Object[]}
*/
export function getParentByChildId(id, Branch, iteratorKey = 'children') {
if (
typeof id !== 'number' ||
typeof Branch !== 'object' ||
typeof iteratorKey !== 'string' ||
!(Branch instanceof Array)
)
return false
let found = false
Branch.map(item => {
if (!found) {
if (iteratorKey in item)
item[iteratorKey].map(child => {
if (!found) {
if ('id' in child && child['id'] === id) {
found = item
} else {
found = getParentByChildId(id, item[iteratorKey])
}
}
})
}
})
return found
}
//
/**
* Takes a flat array and makes it multidimensional
* @param {Object[]} FlatTree - flat array to be inflated
* @param {String} [idTag=id]
* @param {String} [parentTag=parent_id]
* @param {String} [childrenTag=children]
* @param {Number} [parent_id=null]
* @param {Number} [level=0]
* @return {Object[]}
*/
export function inflate(
FlatTree,
idTag = 'id',
parentTag = 'parent_id',
childrenTag = 'children',
parent_id = null,
level = 0
) {
if (
!(FlatTree instanceof Array) ||
typeof idTag !== 'string' ||
typeof parentTag !== 'string' ||
typeof childrenTag !== 'string'
)
return false
let branch = []
FlatTree.map(_item => {
let item = cloneDeep(_item)
if (idTag in item && parentTag in item && item[parentTag] === parent_id) {
const children = inflate(FlatTree, idTag, parentTag, childrenTag, item[idTag], level + 1)
item[childrenTag] = children && children instanceof Array ? children : []
// item.level = level;
branch.push(item)
}
})
return branch
}
/**
* Takes a multidimensional array and flattens it
* @param {Object[]} Tree
* @param {String} [iteratorKey=children]
* @param {Number} [level=0]
* @return {Object[]}
*/
export function flatten(Tree, iteratorKey = 'children', level = 0) {
if (!(Tree instanceof Array) || typeof iteratorKey !== 'string') return false
return Tree.reduce((list, branch) => {
const rest = {}
Object.keys(branch).map(key => {
if (key !== iteratorKey) rest[key] = branch[key]
})
list.push(rest)
if (iteratorKey in branch) {
list = list.concat(flatten(branch[iteratorKey], iteratorKey, level + 1))
}
return list
}, [])
}
/**
* Takes a multidimensional array and flattens it, excluding certain ids
* @param {Object[]} Tree
* @param {Number[]} ignoreIds
* @param {String} [iteratorKey=children]
* @param {Number} [level=0]
* @return {Object[]}
*/
export function flattenWithoutCollapsed(Tree, ignoreIds = [], iteratorKey = 'children', level = 0) {
if (!(Tree instanceof Array) || !(ignoreIds instanceof Array) || typeof iteratorKey !== 'string') return false
return Tree.reduce((list, branch) => {
const rest = {}
Object.keys(branch).map(key => {
if (key !== iteratorKey) rest[key] = branch[key]
})
list.push(rest)
if (iteratorKey in branch && ignoreIds.indexOf(branch.id) < 0) {
list = list.concat(flattenWithoutCollapsed(branch[iteratorKey], ignoreIds, iteratorKey, level + 1))
}
return list
}, [])
}
// takes a multidimensional array and flattens it
/**
* Takes a multidimensional array and flattens it with 'path' array
* @param {Object[]} branches
* @param {Array} path
* @return {Object[]}
*/
export function flattenWithPath(branches, path = []) {
let index = 0
return branches.reduce((list, branch) => {
const { children, ...rest } = branch
let newPath = path.slice()
if (path.length) newPath.push('children')
newPath.push(index++)
const item = {
item: { ...rest, children },
path: newPath,
}
list.push(item)
if (children && children.length) {
list = list.concat(flattenWithPath(children, newPath))
}
return list
}, [])
}
/**
* @param {Object[]} branches
* @param {Number} [parent_id=null]
* @return {Object[]}
*/
export function flattenWithParentIds(branches, parent_id = null) {
return branches.reduce((list, branch) => {
const { children, ...rest } = branch
const item = { ...rest, parent_id }
list.push(item)
if (children && children.length) {
list = list.concat(flattenWithParentIds(children, item.id))
}
return list
}, [])
}
/**
* Prunes a tree's branches based on a key and an array of disallowed values
* @param {Object[]} _tree
* @param {String} key
* @param {String[]} values
* @param {String} [iteratorKey=children]
* @return {Object[]}
*/
export function pruneTree(_tree, key, values, iteratorKey = 'children') {
if (!(_tree instanceof Array) || typeof key !== 'string' || typeof iteratorKey !== 'string') return false
if (!(values instanceof Array)) values = [values]
let tree = cloneDeep(_tree)
tree = tree.filter(branch => values.indexOf(branch[key]) < 0)
tree = tree.map(branch => {
if (iteratorKey in branch && branch[iteratorKey] instanceof Array)
branch[iteratorKey] = pruneTree(branch[iteratorKey], key, values, iteratorKey)
return branch
})
return tree
}
/**
* Recursively iterates through tree using a checker function to determine whether or not to add a branch's key to an array of values
* @param {Object[]} tree
* @param {String} key
* @param {Function} checker
* @param {String} [iteratorKey=children]
* @return {Array} - returns array of values
*/
export function collectValues(tree, key, checker, iteratorKey = 'children') {
if (
!(tree instanceof Array) ||
typeof key !== 'string' ||
typeof checker !== 'function' ||
typeof iteratorKey !== 'string'
)
return false
let values = []
tree.map(branch => {
if (checker(branch) && key in branch) values.push(branch[key])
if (iteratorKey in branch && branch[iteratorKey] instanceof Array)
values = values.concat(collectValues(branch[iteratorKey], key, checker, iteratorKey))
})
return values
}
/**
* Recursively iterates through tree to get a child by a key and a value
* @param {Object[]} tree
* @param {String} key
* @param {*} value - Can be anything
* @param {String} [iteratorKey=children]
* @return {Object}
*/
export function getChildByKeyValue(tree, key, value, iteratorKey = 'children') {
if (!(tree instanceof Array) || typeof key !== 'string' || typeof iteratorKey !== 'string') return false
let found = false
tree.map(item => {
if (!found) {
if (item[key] === value) found = item
else if (item[iteratorKey]) found = getChildByKeyValue(item[iteratorKey], key, value, iteratorKey)
}
})
return found
}
/**
* Recursively iterates through tree and get all values of a set key
* @param {Object[]} _tree
* @param {Number} id
* @param {String} key
* @param {String} [iteratorKey=children]
* @return {String[]}
*/
export function collectChildrenKeyValues(_tree, id, key, iteratorKey = 'children') {
if (!(_tree instanceof Array) || typeof key !== 'string' || typeof id !== 'number' || typeof iteratorKey !== 'string')
return false
const branch = getChildByKeyValue(_tree, 'id', id, iteratorKey)
if (!branch[iteratorKey]) return []
return collectValues(branch[iteratorKey], key, item => true, iteratorKey)
}
/**
* Recursively iterates through tree looking for a child and sets a value by key, returns new tree
* @param {Object[]} _tree
* @param {Number} id
* @param {String} setKey
* @param {*} setValue - Can be anything
* @param {Object[]}
*/
export function setKeyValueById(_tree, id, setKey, setValue, iteratorKey = 'children') {
let tree = cloneDeep(_tree)
return tree.map(item => {
if (item.id === id) item[setKey] = setValue
if (item[iteratorKey]) item[iteratorKey] = setKeyValueById(item[iteratorKey], id, setKey, setValue, iteratorKey)
return item
})
}
/**
* Recursively iterates through tree and sorts all branches by key, returns new tree
* @param {Object[]} _tree
* @param {String} key
* @param {Boolean} desc - Descending
* @param {String} [iteratorKey=children]
* @return {Object[]}
*/
export function sortByKey(_tree, key, desc = false, iteratorKey = 'children') {
let tree = _tree.slice()
tree.sort((a, b) => {
if (typeof a[key] == 'string') {
return a[key].localeCompare(b[key], undefined, { sensitivity: 'base' })
} else {
return a[key] - b[key]
}
})
if (desc) tree.reverse()
return tree.map(item => {
if (!item[iteratorKey]) {
return item
}
return {
...item,
[iteratorKey]: sortByKey(item[iteratorKey], key, desc, iteratorKey),
}
})
}
/**
* Recursively iterates through tree and appends child to parent by parentId, returns new tree
* @param {Object[]} _tree
* @param {Number} parentId
* @param {Object} child
* @param {Number} [currentParentId=null]
* @param {String} [iteratorKey=children]
* @return {Object[]}
*/
export function appendChildToParent(_tree, parentId, child, currentParentId = null, iteratorKey = 'children') {
if (!(_tree instanceof Array) || typeof iteratorKey !== 'string') return false
let tree = cloneDeep(_tree)
if (parentId === currentParentId) tree.push(child)
else
tree = tree.map(item => {
if (parentId === item.id && !item[iteratorKey]) item[iteratorKey] = []
if (item[iteratorKey])
item[iteratorKey] = appendChildToParent(item[iteratorKey], parentId, child, item.id, iteratorKey)
return item
})
return tree
}
/**
* Recursively iterates through tree looking to replace a specific child by given key (children are preserved), returns new tree
* @param {Object[]} _tree
* @param {Object} child
* @param {String} [key=id]
* @param {String} [iteratorKey=children]
* @return {Object[]}
*/
export function replaceChild(_tree, child, key = 'id', iteratorKey = 'children') {
if (!(_tree instanceof Array) || typeof iteratorKey !== 'string') return false
let tree = cloneDeep(_tree)
return tree.map(item => {
if (item[key] === child[key]) {
if (item[iteratorKey] && item[iteratorKey].length) return { ...child, [iteratorKey]: item[iteratorKey] }
else return child
}
if (item[iteratorKey]) item[iteratorKey] = replaceChild(item[iteratorKey], child, key, iteratorKey)
return item
})
}
/**
* Recursively iterates through tree and concats branch chilren where supplied values match branch value
* @param {Object[]} _tree
* @param {Object[]} values
* @param {String} valueKey
* @param {String} key
* @param {String} iteratorKey
* @return {Object[]}
*/
export function mergeChildren(_tree, values, valueKey = 'folder_id', key = 'id', iteratorKey = 'children') {
if (!(_tree instanceof Array) || !(values instanceof Array)) return false
let tree = cloneDeep(_tree)
const orphanedValues = values.filter(value => value[valueKey] === null)
if (orphanedValues.length > 0) tree = [...tree, ...orphanedValues.map(value => ({ ...value, __injected: true }))]
return tree.map(item => {
if (item.__injected) return item
const injectValues = values.filter(value => value[valueKey] == item[key])
if (item[iteratorKey] && item[iteratorKey].length > 0) {
item[iteratorKey] = [...mergeChildren(item[iteratorKey], values, valueKey, key, iteratorKey), ...injectValues]
} else {
item[iteratorKey] = injectValues
}
return item
})
}