@vectara/vectara-ui
Version:
Vectara's design system, codified as a React and Sass component library
70 lines (69 loc) • 3.09 kB
JavaScript
// Flattens a list of rows (linked by `parentId`) into a render-ordered, depth-tagged array,
// showing only visible nodes based on `expandedIds`. The render layer maps this 1:1 to <tr>s.
// Orphans (rows with missing parents) are dropped. Cycles are avoided using an ancestor set.
export const buildAndFlattenSpans = (rows, expandedIds, getId, getParentId) => {
// First pass: collect every known id so we can detect orphans below.
const allIds = new Set();
for (const row of rows) {
allIds.add(getId(row));
}
// Group rows by their parent id, with top-level rows under a sentinel key.
// The sentinel can't collide with a real id because it contains spaces.
const ROOT_KEY = " __VuiSpansRoot__ ";
const childrenByParent = new Map();
for (const row of rows) {
const parentId = getParentId(row);
// Drop orphans: parent id set but not present in the rows list.
if (parentId !== null && !allIds.has(parentId))
continue;
const key = parentId === null ? ROOT_KEY : parentId;
const list = childrenByParent.get(key);
if (list) {
list.push(row);
}
else {
childrenByParent.set(key, [row]);
}
}
const result = [];
// Recursive depth-first walk. `ancestors` carries the chain of ids from the
// root down to (but not including) the current row, used to detect cycles.
const walk = (parentKey, parentId, depth, ancestors) => {
const siblings = childrenByParent.get(parentKey);
if (!siblings)
return;
siblings.forEach((row, index) => {
const id = getId(row);
// Break cycle: this id appears earlier in its own ancestor chain.
if (ancestors.has(id))
return;
const ownChildren = childrenByParent.get(id);
const ownChildCount = ownChildren ? ownChildren.length : 0;
const hasLoadedChildren = ownChildCount > 0;
// The chevron should appear if either we already have children to show
// or the row is hinting that lazily-loadable children exist.
const hasChildren = row.hasChildren || hasLoadedChildren;
result.push({
row,
id,
parentId,
depth,
hasChildren,
hasLoadedChildren,
posInSet: index + 1,
setSize: siblings.length
});
// Only descend if the user has expanded this row AND there's something
// loaded to descend into. The lazy-only case (`hasChildren` but
// `!hasLoadedChildren`) is handled by the loading row in the render
// layer, not here.
if (hasChildren && expandedIds.has(id) && ownChildCount > 0) {
const nextAncestors = new Set(ancestors);
nextAncestors.add(id);
walk(id, id, depth + 1, nextAncestors);
}
});
};
walk(ROOT_KEY, null, 0, new Set());
return result;
};