/** * Utility class for working with trees * * @author Sergey Shishigin * @date 2019-10-21 */ import {IFlatListItem} from '../../component/'; import {ITreeNode} from '../tree/Tree'; export class TreeUtil { /** * Method for converting a flat list to a tree * inspired by * https://github.com/philipstanislaus/performant-array-to-tree/blob/master/src/arrayToTree.ts * https://github.com/philipstanislaus/performant-array-to-tree * * @param items {IFlatListItem[]} FlatListItem * @param onItemAdd */ public static listToTree ( items: Array>, onItemAdd?: (item: T, node: ITreeNode) => void ): Array> { let rootItems: Array> = [], parentNode: ITreeNode; // stores all already processed items with ther ids as key so we can easily look them up const lookup: { [id: string]: ITreeNode } = {}; // idea of this loop: // whenever an item has a parent, but the parent is not yet in the lookup object, we store a preliminary parent // in the lookup object and fill it with the data of the parent later // if an item has no parentId, add it as a root element to rootItems for (const item of items) { const itemId = item.nodeId, parentId = item.parentId; // look whether item already exists in the lookup table if (!Object.prototype.hasOwnProperty.call(lookup, itemId)) { // item is not yet there, so add a preliminary item (its data will be added later) lookup[itemId] = { key: itemId, path: item.path, expanded: false, children: null, row: item.model }; } else { Object.assign(lookup[itemId], { key: itemId, path: item.path, row: item.model }); } const node = lookup[itemId]; if (parentId === null) { // is a root item rootItems.push(node); } else { // has a parent // look whether the parent already exists in the lookup table if (!Object.prototype.hasOwnProperty.call(lookup, parentId)) { // parent is not yet there, so add a preliminary parent (its data will be added later) lookup[parentId] = { key: parentId, path: parentId, row: {}, expanded: false, children: null }; } // add the current item to the parent parentNode = lookup[parentId]; lookup[itemId].parent = parentNode; parentNode.children = parentNode.children || []; parentNode.children.push(node); } if (onItemAdd) { onItemAdd(item.model, node); } } return rootItems; } /** * Expand the entire tree (starting at any level) * @param tree */ public static expandAll (tree: Array>): void { const setExpand = (node: ITreeNode) => { node.expanded = true; if (node.children) { node.children.forEach((childNode) => { setExpand(childNode); }); } }; tree.forEach((node) => { setExpand(node); }); } /** * Collapse the entire tree (starting at any level) * @param tree */ public static collapseAll (tree: Array>): void { const setCollapse = (node: ITreeNode) => { node.expanded = false; if (node.children) { node.children.forEach((childNode) => { setCollapse(childNode); }); } }; tree.forEach((node) => { setCollapse(node); }); } /** * Sorting the tree and nested sub trees * @param a * @param b */ public static sortTree (a: ITreeNode, b: ITreeNode): number { if (a.children && b.children) { if (a.children.length > b.children.length) { return -1; } else { return 1; } } else if (a.children) { return -1; } else if (b.children) { return 1; } return a.row.displayName > b.row.displayName ? 1 : -1; } public static forEachTreeNode ( handler: (item: ITreeNode) => void, currentList: Array> ): void { currentList.forEach((item) => { const children = item.children; if (children && children.length) { TreeUtil.forEachTreeNode(handler, children); } handler(item); }); } public static flatTree (nodes: Array>): Array> { const list: Array> = []; TreeUtil.forEachTreeNode((node) => { list.push(node); }, nodes); return list; } public static mapTree ( handler: (item: ITreeNode) => ITreeNode, currentList: Array> ): Array> { const clonnedNodes: Array> = []; currentList.forEach((item) => { const children = item.children, clone = {...item, row: {...item.row}}; if (item.parent) { clone.parent = {...item.parent}; } if (children && children.length) { clone.children = this.mapTree(handler, children); } clonnedNodes.push(handler(clone)); }); return clonnedNodes; } public static cloneTree ( nodes: Array> ): Array> { return TreeUtil.mapTree((node) => node, nodes); } public static filterTree ( handler: (item: ITreeNode) => boolean, currentList: Array> ): Array> { const filteredNodes: Array> = []; currentList.forEach((item) => { const children = item.children; let filteredChildren: Array> = []; if (children && children.length) { filteredChildren = this.filterTree(handler, children); } if (filteredChildren.length) { filteredNodes.push({...item, children: filteredChildren}); } else if (handler(item)) { filteredNodes.push({...item, children: null}); } }); return filteredNodes; } public static findTreeNode ( handler: (item: ITreeNode) => boolean, currentList: Array> ): ITreeNode | undefined { let result: ITreeNode | undefined; currentList.forEach((item) => { const children = item.children; if (!result && children && children.length) { result = TreeUtil.findTreeNode(handler, children); } if (!result && handler(item)) { result = item; } }); return result; } public static findParentNode ( nodes: Array>, itemKey: string ): ITreeNode | undefined { return TreeUtil.findTreeNode(((item) => { return Boolean(item.children && item.children.length && item.children.find((node) => node.key === itemKey)); }), nodes); } /** * @deprecated */ public static initParents (parentNode: ITreeNode, recursive?: boolean): void { const children = parentNode.children; if (children) { const length = children.length; children.forEach((child: ITreeNode, index) => { child.parent = parentNode; child.isLastInLevel = index === length - 1; if (recursive) { this.initParents(child, true); } }); } } }