Skip to content

Commit bf9695e

Browse files
committed
Create function to flatten tree data into a list of visible nodes
1 parent 45bcb73 commit bf9695e

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

src/utils/tree-data-utils.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,69 @@ export function getVisibleNodeInfoAtIndex(data, targetIndex, getNodeKey) {
104104
return null;
105105
}
106106

107+
/**
108+
* Get visible node data flattened.
109+
*
110+
* @param {!Object[]} data - Tree data
111+
* @param {!number} targetIndex - The index of the node to search for
112+
* @param {function} getNodeKey - Function to get the key from the nodeData and tree index
113+
*
114+
* @return {{
115+
* node: Object,
116+
* parentPath: []string|[]number,
117+
* lowerSiblingCounts: []number
118+
* }}[] nodes - The node array
119+
*/
120+
export function getVisibleNodeInfoFlattened(treeData, getNodeKey) {
121+
if (!treeData || treeData.length < 1) {
122+
return [];
123+
}
124+
125+
const trav = ({
126+
node,
127+
currentIndex,
128+
parentPath = [],
129+
lowerSiblingCounts = [],
130+
isPseudoRoot = false,
131+
}) => {
132+
const selfInfo = !isPseudoRoot ? [{ node, parentPath, lowerSiblingCounts }] : [];
133+
134+
// Add one and continue for nodes with no children or hidden children
135+
if (!node.children || (node.expanded !== true && !isPseudoRoot)) {
136+
return selfInfo;
137+
}
138+
139+
// Iterate over each child and their ancestors and return the
140+
// target node if childIndex reaches the targetIndex
141+
let childIndex = currentIndex + 1;
142+
const childCount = node.children.length;
143+
const results = [];
144+
const selfKey = !isPseudoRoot ? getNodeKey(node, currentIndex) : null;
145+
for (let i = 0; i < childCount; i++) {
146+
results[i] = trav({
147+
node: node.children[i],
148+
currentIndex: childIndex,
149+
lowerSiblingCounts: [ ...lowerSiblingCounts, childCount - i - 1 ],
150+
151+
// The pseudo-root, with a null parent path, is not considered in the parent path
152+
parentPath: !isPseudoRoot ? [ ...parentPath, selfKey ] : [],
153+
});
154+
155+
childIndex += results[i].length;
156+
}
157+
158+
return selfInfo.concat(...results);
159+
};
160+
161+
return trav({
162+
node: { children: treeData },
163+
currentIndex: -1,
164+
parentPath: [],
165+
isPseudoRoot: true,
166+
lowerSiblingCounts: [],
167+
});
168+
}
169+
107170
/**
108171
* Replaces node at path with object, or callback-defined object
109172
*

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

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
getVisibleNodeCount,
33
getVisibleNodeInfoAtIndex,
44
changeNodeAtPath,
5+
getVisibleNodeInfoFlattened,
56
} from './tree-data-utils';
67

78
const keyFromTreeIndex = (node, treeIndex) => treeIndex;
@@ -213,6 +214,125 @@ describe('getVisibleNodeInfoAtIndex', () => {
213214
});
214215
});
215216

217+
218+
describe('getVisibleNodeInfoAtIndex', () => {
219+
it('should handle empty data', () => {
220+
expect(getVisibleNodeInfoFlattened([], keyFromTreeIndex)).toEqual([]);
221+
expect(getVisibleNodeInfoFlattened(null, keyFromTreeIndex)).toEqual([]);
222+
expect(getVisibleNodeInfoFlattened(undefined, keyFromTreeIndex)).toEqual([]);
223+
});
224+
225+
it('should handle flat data', () => {
226+
expect(getVisibleNodeInfoFlattened([ { key: 0 } ], keyFromTreeIndex)).toEqual([
227+
{ node: { key: 0 }, parentPath: [], lowerSiblingCounts: [ 0 ] },
228+
]);
229+
expect(getVisibleNodeInfoFlattened([ { key: 0 }, { key: 1 } ], keyFromTreeIndex)).toEqual([
230+
{ node: { key: 0 }, parentPath: [], lowerSiblingCounts: [ 1 ] },
231+
{ node: { key: 1 }, parentPath: [], lowerSiblingCounts: [ 0 ] }
232+
]);
233+
});
234+
235+
it('should handle hidden nested data', () => {
236+
const treeData = [
237+
{
238+
key: 0,
239+
children: [
240+
{
241+
key: 1,
242+
children: [
243+
{ key: 2 },
244+
{ key: 3 },
245+
],
246+
},
247+
{
248+
key: 4,
249+
children: [
250+
{ key: 5 },
251+
],
252+
},
253+
],
254+
},
255+
{ key: 6 },
256+
];
257+
258+
expect(getVisibleNodeInfoFlattened(treeData, keyFromTreeIndex)).toEqual([
259+
{ node: treeData[0], parentPath: [], lowerSiblingCounts: [ 1 ] },
260+
{ node: treeData[1], parentPath: [], lowerSiblingCounts: [ 0 ] }
261+
]);
262+
});
263+
264+
it('should handle partially expanded nested data', () => {
265+
const treeData = [
266+
{
267+
expanded: true,
268+
key: 0,
269+
children: [
270+
{
271+
key: 1,
272+
children: [
273+
{ key: 2 },
274+
{ key: 3 },
275+
],
276+
},
277+
{
278+
expanded: true,
279+
key: 4,
280+
children: [
281+
{ key: 5 },
282+
],
283+
},
284+
],
285+
},
286+
{ key: 6 },
287+
];
288+
289+
expect(getVisibleNodeInfoFlattened(treeData, keyFromKey)).toEqual([
290+
{ node: treeData[0], parentPath: [], lowerSiblingCounts: [ 1 ] },
291+
{ node: treeData[0].children[0], parentPath: [0], lowerSiblingCounts: [ 1, 1 ] },
292+
{ node: treeData[0].children[1], parentPath: [0], lowerSiblingCounts: [ 1, 0 ] },
293+
{ node: treeData[0].children[1].children[0], parentPath: [0, 4], lowerSiblingCounts: [ 1, 0, 0 ] },
294+
{ node: treeData[1], parentPath: [], lowerSiblingCounts: [ 0 ] },
295+
]);
296+
});
297+
298+
it('should handle fully expanded nested data', () => {
299+
const treeData = [
300+
{
301+
expanded: true,
302+
key: 0,
303+
children: [
304+
{
305+
expanded: true,
306+
key: 1,
307+
children: [
308+
{ key: 2 },
309+
{ key: 3 },
310+
],
311+
},
312+
{
313+
expanded: true,
314+
key: 4,
315+
children: [
316+
{ key: 5 },
317+
],
318+
},
319+
],
320+
},
321+
{ key: 6 },
322+
];
323+
324+
expect(getVisibleNodeInfoFlattened(treeData, keyFromTreeIndex)).toEqual([
325+
{ node: treeData[0], parentPath: [], lowerSiblingCounts: [1] },
326+
{ node: treeData[0].children[0], parentPath: [0], lowerSiblingCounts: [1, 1] },
327+
{ node: treeData[0].children[0].children[0], parentPath: [0, 1], lowerSiblingCounts: [1, 1, 1] },
328+
{ node: treeData[0].children[0].children[1], parentPath: [0, 1], lowerSiblingCounts: [1, 1, 0] },
329+
{ node: treeData[0].children[1], parentPath: [0], lowerSiblingCounts: [1, 0] },
330+
{ node: treeData[0].children[1].children[0], parentPath: [0, 4], lowerSiblingCounts: [1, 0, 0] },
331+
{ node: treeData[1], parentPath: [], lowerSiblingCounts: [0] },
332+
]);
333+
});
334+
});
335+
216336
describe('changeNodeAtPath', () => {
217337
it('should handle empty data', () => {
218338
expect(() => changeNodeAtPath([], [1], {}, keyFromTreeIndex)).toThrow();

0 commit comments

Comments
 (0)