Skip to content

Commit ab9e53c

Browse files
committed
Create find util to search for nodes in the tree
1 parent 587e0d6 commit ab9e53c

File tree

2 files changed

+213
-1
lines changed

2 files changed

+213
-1
lines changed

src/utils/tree-data-utils.js

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function getNodeDataAtTreeIndexOrNextIndex({
2929
return { nextIndex: currentIndex + 1 };
3030
}
3131

32-
// Iterate over each child and their ancestors and return the
32+
// Iterate over each child and their descendants and return the
3333
// target node if childIndex reaches the targetIndex
3434
let childIndex = currentIndex + 1;
3535
const childCount = node.children.length;
@@ -824,3 +824,132 @@ export function getDepth(node, depth = 0) {
824824
depth
825825
);
826826
}
827+
828+
/**
829+
* Find nodes matching a search query in the tree,
830+
*
831+
* @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
832+
* @param {!Object[]} treeData - Tree data
833+
* @param {?string|number} searchQuery - Function returning a boolean to indicate whether the node is a match or not
834+
* @param {!function} searchMethod - Function returning a boolean to indicate whether the node is a match or not
835+
* @param {?number} searchFocusOffset - The offset of the match to focus on
836+
* (e.g., 0 focuses on the first match, 1 on the second)
837+
* @param {boolean=} expandAllMatchPaths - If true, expands the paths to any matched node
838+
* @param {boolean=} expandFocusMatchPaths - If true, expands the path to the focused node
839+
*
840+
* @return {Object[]} matches - An array of objects containing the matching `node`s, their `path`s and `treeIndex`s
841+
* @return {Object[]} treeData - The original tree data with all relevant nodes expanded.
842+
* If expandAllMatchPaths and expandFocusMatchPaths are both false,
843+
* it will be the same as the original tree data.
844+
*/
845+
export function find({
846+
getNodeKey,
847+
treeData,
848+
searchQuery,
849+
searchMethod,
850+
searchFocusOffset,
851+
expandAllMatchPaths = false,
852+
expandFocusMatchPaths = true,
853+
}) {
854+
const matches = [];
855+
856+
const trav = ({
857+
isPseudoRoot = false,
858+
node,
859+
currentIndex,
860+
path = [],
861+
}) => {
862+
let hasMatch = false;
863+
let hasFocusMatch = false;
864+
// The pseudo-root is not considered in the path
865+
const selfPath = isPseudoRoot ? [] : [
866+
...path,
867+
getNodeKey({ node, treeIndex: currentIndex }),
868+
];
869+
const extraInfo = isPseudoRoot ? null : {
870+
path: selfPath,
871+
treeIndex: currentIndex,
872+
};
873+
874+
// Nodes with with children that aren't lazy
875+
const hasChildren = node.children &&
876+
typeof node.children !== 'function' &&
877+
node.children.length > 0;
878+
879+
let childIndex = currentIndex;
880+
const newNode = { ...node };
881+
882+
// Examine the current node to see if it is a match
883+
if (!isPseudoRoot && searchMethod({ ...extraInfo, node: newNode, searchQuery })) {
884+
hasMatch = true;
885+
if (matches.length === searchFocusOffset) {
886+
hasFocusMatch = true;
887+
if ((expandAllMatchPaths || expandFocusMatchPaths) && hasChildren) {
888+
newNode.expanded = true;
889+
}
890+
}
891+
892+
matches.push({ ...extraInfo, node: newNode });
893+
}
894+
895+
if (hasChildren) {
896+
// Get all descendants
897+
newNode.children = newNode.children.map((child) => {
898+
const mapResult = trav({
899+
node: child,
900+
currentIndex: childIndex + 1,
901+
path: selfPath,
902+
});
903+
904+
// Ignore hidden nodes by only advancing the index counter to the returned treeIndex
905+
// if the child is expanded.
906+
//
907+
// The child could have been expanded from the start,
908+
// or expanded due to a matching node being found in its descendants
909+
if (mapResult.node.expanded) {
910+
childIndex = mapResult.treeIndex;
911+
} else {
912+
childIndex += 1;
913+
}
914+
915+
if (mapResult.hasMatch || mapResult.hasFocusMatch) {
916+
hasMatch = true;
917+
918+
if (mapResult.hasFocusMatch) {
919+
hasFocusMatch = true;
920+
}
921+
922+
// Expand the current node if it has descendants matching the search
923+
// and the settings are set to do so.
924+
if ((expandAllMatchPaths && mapResult.hasMatch) ||
925+
((expandAllMatchPaths || expandFocusMatchPaths) && mapResult.hasFocusMatch)
926+
) {
927+
newNode.expanded = true;
928+
}
929+
}
930+
931+
return mapResult.node;
932+
});
933+
} else {
934+
childIndex += 1;
935+
}
936+
937+
return {
938+
node: hasMatch ? newNode : node,
939+
hasMatch,
940+
hasFocusMatch,
941+
treeIndex: childIndex,
942+
};
943+
};
944+
945+
const result = trav({
946+
node: { children: treeData },
947+
isPseudoRoot: true,
948+
currentIndex: -1,
949+
});
950+
951+
return {
952+
matches,
953+
treeData: result.node.children,
954+
};
955+
}

src/utils/tree-data-utils.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isDescendant,
1313
getDepth,
1414
getDescendantCount,
15+
find,
1516
} from './tree-data-utils';
1617

1718
const keyFromTreeIndex = ({ treeIndex }) => treeIndex;
@@ -1774,3 +1775,85 @@ describe('getDescendantCount', () => {
17741775
expect(getDescendantCount({ ignoreCollapsed: true, node: nested })).toEqual(3);
17751776
});
17761777
});
1778+
1779+
describe('find', () => {
1780+
const commonArgs = {
1781+
searchQuery: 42,
1782+
searchMethod: ({ node, searchQuery }) => (node.key === searchQuery),
1783+
expandAllMatchPaths: false,
1784+
expandFocusMatchPaths: true,
1785+
getNodeKey: keyFromKey,
1786+
searchFocusOffset: 0,
1787+
};
1788+
1789+
it('should work with flat data', () => {
1790+
let result;
1791+
1792+
result = find({ ...commonArgs, treeData: [{}] });
1793+
expect(result.matches).toEqual([]);
1794+
1795+
result = find({ ...commonArgs, treeData: [{ key: 41 }] });
1796+
expect(result.matches).toEqual([]);
1797+
1798+
result = find({ ...commonArgs, treeData: [{ key: 42 }] });
1799+
expect(result.matches).toEqual([
1800+
{ node: { key: 42 }, treeIndex: 0, path: [42] },
1801+
]);
1802+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(0);
1803+
1804+
result = find({ ...commonArgs, treeData: [{ key: 41 }, { key: 42 }] });
1805+
expect(result.matches).toEqual([
1806+
{ node: { key: 42 }, treeIndex: 1, path: [42] },
1807+
]);
1808+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(1);
1809+
1810+
result = find({ ...commonArgs, treeData: [{ key: 42 }, { key: 42 }] });
1811+
expect(result.matches).toEqual([
1812+
{ node: { key: 42 }, treeIndex: 0, path: [42] },
1813+
{ node: { key: 42 }, treeIndex: 1, path: [42] },
1814+
]);
1815+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(0);
1816+
});
1817+
1818+
it('should work with nested data', () => {
1819+
let result;
1820+
1821+
result = find({
1822+
...commonArgs,
1823+
treeData: [{ children: [{ key: 42 }] }],
1824+
});
1825+
expect(result.matches.length).toEqual(1);
1826+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(1);
1827+
1828+
result = find({
1829+
...commonArgs,
1830+
treeData: [{ children: [{ key: 41 }] }, { children: [{ key: 42 }] }],
1831+
});
1832+
expect(result.matches.length).toEqual(1);
1833+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(2);
1834+
expect(result.treeData).toEqual(
1835+
[{ children: [{ key: 41 }] }, { expanded: true, children: [{ key: 42 }] }]
1836+
);
1837+
1838+
result = find({
1839+
...commonArgs,
1840+
treeData: [{ children: [{ children: [{ key: 42 }] }] }],
1841+
});
1842+
expect(result.matches.length).toEqual(1);
1843+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(2);
1844+
1845+
result = find({
1846+
...commonArgs,
1847+
treeData: [{ children: [{ key: 42, children: [{ key: 42 }] }] }],
1848+
});
1849+
expect(result.matches.length).toEqual(2);
1850+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(1);
1851+
1852+
result = find({
1853+
...commonArgs,
1854+
treeData: [{}, { children: [{ key: 42, expanded: true, children: [{ key: 42 }] }] }],
1855+
});
1856+
expect(result.matches.length).toEqual(2);
1857+
expect(result.matches[commonArgs.searchFocusOffset].treeIndex).toEqual(2);
1858+
});
1859+
});

0 commit comments

Comments
 (0)