@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
402 lines • 13.9 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
import React, { useContext } from 'react';
import { NODE_ID_ATTRIBUTE_NAME } from '../TreeNodeBase/TreeNodeBase.constants';
import { DEFAULTS } from './Tree.constants';
export var TreeContext = React.createContext(null);
/**
* Get the tree context value.
* It throws an error if the context is not provided.
*/
export var useTreeContext = function () {
var value = useContext(TreeContext);
if (!value) {
// eslint-disable-next-line no-console
console.error('useTreeContext hook used without TreeContext!');
}
return value;
};
/**
* Get the root node id of the tree which is represented as a map.
*
* @param tree
*/
export var getTreeRootId = function (tree) {
var _a;
return (_a = Array.from(tree.values()).find(function (node) { return !node.parent; })) === null || _a === void 0 ? void 0 : _a.id;
};
/**
* Check if the tree is empty.
*
* Works with both Map and recursive Object tree representation.
*
* @param tree
*/
export var isEmptyTree = function (tree) {
if (!tree)
return true;
if (tree instanceof Map && tree.size !== 0)
return false;
if (tree instanceof Object && tree['id'])
return false;
return true;
};
/**
* Find the next active tree node based on the current active node
* @param tree
* @param activeNodeId
* @internal
*/
var findNextTreeNode = function (tree, activeNodeId) {
var current = tree.get(activeNodeId);
// Step into an open node
if (!current.isLeaf && current.isOpen) {
return { action: 'move', nextNodeId: current.children[0] };
}
var loopCheck = new Set();
// Otherwise, find the next sibling
// eslint-disable-next-line no-constant-condition
while (true) {
var parent_1 = tree.get(current.parent);
var pos = current.index + 1;
// Reached the last node of the tree
if (!parent_1) {
return { action: 'noop', nodeId: activeNodeId };
}
else if (parent_1.children[pos]) {
return { action: 'move', nextNodeId: parent_1.children[pos] };
}
else {
// If we are at the end of the parent's children, move up one level
current = parent_1;
if (loopCheck.has(current.id)) {
// eslint-disable-next-line no-console
console.error('Infinite loop detected in the tree navigation.');
return { action: 'move', nextNodeId: current.id };
}
else {
loopCheck.add(current.id);
}
}
}
};
/**
* Find the previous active tree node based on the current active node
*
* @param tree
* @param excludeRootNode
* @param activeNodeId
* @internal
*/
var findPreviousTreeNode = function (tree, excludeRootNode, activeNodeId) {
var current = tree.get(activeNodeId);
// Already in the root
if (!current.parent)
return { action: 'noop', nodeId: activeNodeId };
// Exclude root
if (current.index === 0 && excludeRootNode && current.parent === getTreeRootId(tree))
return { action: 'noop', nodeId: activeNodeId };
// Move one level up
if (current.index === 0)
return { action: 'move', nextNodeId: current.parent };
// Find the previous sibling
var next = tree.get(tree.get(current.parent).children[current.index - 1]);
var loopCheck = new Set(activeNodeId);
for (var counter = 0; next; counter++) {
if (next.isLeaf || !next.isOpen) {
return { action: 'move', nextNodeId: next.id };
}
// Last child of the open node
next = tree.get(next.children[next.children.length - 1]);
if (loopCheck.has(next.id)) {
// eslint-disable-next-line no-console
console.error('Infinite loop detected in the tree navigation.');
return { action: 'move', nextNodeId: next.id };
}
else {
loopCheck.add(next.id);
}
}
};
/**
* Open or find the next node based on the current active node
*
* @param tree
* @param activeNodeId
* @internal
*/
var openNextNode = function (tree, activeNodeId) {
var current = tree.get(activeNodeId);
if (!current.isLeaf) {
if (!current.isOpen) {
// Open it if it's closed
return { action: 'open', nodeId: activeNodeId };
}
else {
// Move to the first child if it's open
return { action: 'move', nextNodeId: current.children[0] };
}
}
// Otherwise, do nothing
return { action: 'noop', nodeId: activeNodeId };
};
/**
* Close or find the next node based on the current active node
*
* @param tree
* @param activeNodeId
* @param excludeRoot
* @internal
*/
var closeNextNode = function (tree, activeNodeId, excludeRoot) {
var current = tree.get(activeNodeId);
// Close the node if it's open and not a leaf
if (current.isOpen && !current.isLeaf) {
return { action: 'close', nodeId: activeNodeId };
}
// Do nothing if it's the root
if (!current.parent || (excludeRoot && current.parent === getTreeRootId(tree))) {
return { action: 'noop', nodeId: activeNodeId };
}
// Move up one level if it's closed
if (current.parent) {
return { action: 'move', nextNodeId: current.parent };
}
// Otherwise, do nothing
return { action: 'noop', nodeId: activeNodeId };
};
/**
* Traverse the tree and convert it to a map between the node id and the node
* It also adds additional information to the node like the parent, the level and the index
*
* @param tree
*/
export var convertNestedTree2MappedTree = function (tree) {
var _a, _b, _c;
var map = new Map();
if (isEmptyTree(tree)) {
return map;
}
var idSet = new Set();
var rootNode = {
node: tree,
parentId: undefined,
level: 0,
index: 0,
isHidden: false,
};
var nodeStack = [rootNode];
var _loop_1 = function () {
var _d = nodeStack.pop(), parentNode = _d.node, parentId = _d.parentId, level = _d.level, index = _d.index, isHidden = _d.isHidden;
if (idSet.has(parentNode.id)) {
// eslint-disable-next-line no-console
console.error("Duplicate node id (\"".concat(parentNode.id.toString(), "\") found and skipped."));
return "continue";
}
else {
idSet.add(parentNode.id);
}
var children = Array.from(new Set(parentNode.children.map(function (n) { return n.id; })));
var isOpen = (_a = parentNode.isOpenByDefault) !== null && _a !== void 0 ? _a : true;
map.set(parentNode.id, {
id: parentNode.id,
isOpen: isOpen,
level: level,
index: index,
children: children,
isHidden: isHidden,
parent: parentId,
isLeaf: !children.length,
});
(_c = (_b = parentNode.children) === null || _b === void 0 ? void 0 : _b.forEach) === null || _c === void 0 ? void 0 : _c.call(_b, function (node, index) {
return nodeStack.push({
node: node,
index: index,
level: level + 1,
parentId: parentNode.id,
isHidden: isHidden || !isOpen,
});
});
};
while (nodeStack.length) {
_loop_1();
}
return map;
};
/**
* Set the next active tree node based on the key code.
*
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ WCAG Tree Pattern}
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-1a/ WCAG Directory Tree example}
*
* @param tree
* @param nodeId Current active tree node descriptor
* @param keyCode Arrow key code
* @param excludeRoot
*/
export var getNextActiveNode = function (tree, nodeId, keyCode, excludeRoot) {
if (excludeRoot === void 0) { excludeRoot = true; }
if (!tree.get(nodeId)) {
console.warn("Tree node not found for id: \"".concat(nodeId, "\"."));
return { action: 'noop', nodeId: nodeId };
}
switch (keyCode) {
case 'ArrowUp':
return findPreviousTreeNode(tree, excludeRoot, nodeId);
case 'ArrowDown':
return findNextTreeNode(tree, nodeId);
case 'ArrowRight':
return openNextNode(tree, nodeId);
case 'ArrowLeft':
return closeNextNode(tree, nodeId, excludeRoot);
default:
return { action: 'noop', nodeId: nodeId };
}
};
/**
* Toggle the open/close state of the tree node.
*
* It also updates the hidden state of all children based on the parent's open state.
* And it returns the updated tree without changing the original tree.
*
* @param id
* @param prevTree
* @param isOpen
* @internal
*/
export var toggleTreeNodeRecord = function (id, prevTree, isOpen) {
var newTree = new Map(prevTree.entries());
var current = prevTree.get(id);
if (current.isOpen === isOpen) {
return prevTree;
}
// Set the new isOpen value if it is provided, otherwise toggle it
newTree.set(id, __assign(__assign({}, current), { isOpen: isOpen }));
// Update the hidden state of all children
mapTree(newTree, function (node, tree) {
var parent = tree.get(node.parent);
newTree.set(node.id, __assign(__assign({}, node), { isHidden: !parent.isOpen || parent.isHidden }));
}, { rootNodeId: id });
return newTree;
};
/**
* Map each tree node to a new value by calling the callback function.
*
* It uses Depth First Search (DFS) with Preorder traverse algorithm.
*
* @param tree The tree to traverse
* @param cb Callback function to process each tree node
* @param options
* @param [options.rootNodeId=undefined] The root node id of the tree
* @param [options.excludeRootNode=true] Include the root node in the result
*/
export var mapTree = function (tree, cb, options) {
var _a, _b;
// Empty tree, do nothing
if (tree.size === 0)
return;
// Get the root node id
var rootNodeId = (_a = options === null || options === void 0 ? void 0 : options.rootNodeId) !== null && _a !== void 0 ? _a : getTreeRootId(tree);
if (!tree.has(rootNodeId)) {
// eslint-disable-next-line no-console
console.error("Tree root node is not found for id: \"".concat(rootNodeId.toString(), "\"."));
return [];
}
var excludeRoot = (_b = options === null || options === void 0 ? void 0 : options.excludeRootNode) !== null && _b !== void 0 ? _b : true;
var result = [];
var idStack = [rootNodeId];
while (idStack.length) {
var nodeId = idStack.shift();
var node = tree.get(nodeId);
if (!(excludeRoot && nodeId === rootNodeId)) {
result.push(cb(node, tree));
}
idStack.unshift.apply(idStack, node.children);
}
return result;
};
/**
* Check if the active node is visible in the tree.
*
* @param treeRef DOM reference of the tree
* @param activeNodeId The id of the active node
*/
export var isActiveNodeInDOM = function (treeRef, activeNodeId) {
return !!treeRef.current.querySelector("[".concat(NODE_ID_ATTRIBUTE_NAME, "=\"").concat(activeNodeId, "\"]"));
};
/**
* Get the initial active node id in the tree.
*
* If selection mode is single and there is only one shown and selected item, it returns the selected item.
* Otherwise, it returns the first shown node in the tree.
*
* @param tree
* @param excludeTreeRoot
* @param itemSelection
*/
export var getInitialActiveNode = function (tree, excludeTreeRoot, itemSelection) {
if (itemSelection.selectionMode === 'single' &&
itemSelection.selectedItems.length === 1 &&
tree.has(itemSelection.selectedItems[0])) {
return itemSelection.selectedItems[0];
}
var rootId = getTreeRootId(tree);
if (rootId) {
var treeNode = tree.get(rootId);
if (excludeTreeRoot && treeNode.isOpen && treeNode.children[0]) {
return treeNode.children[0];
}
if (!excludeTreeRoot) {
return rootId;
}
}
return undefined;
};
/**
* Get the DOM id of the tree node.
*
* Node id prefixed with a constant to ensure the id really used only once in the DOM
* @param id
*/
export var getNodeDOMId = function (id) {
return "".concat(DEFAULTS.NODE_ID_PREFIX, "-").concat(id);
};
/**
* Migrate states between old and new trees
*
* `isOpen` state used from the old tree if the node available otherwise falls back to the new tree's node value.
* `isHidden` also updated based on the merged `isOpen` state.
*
* @remarks
* This function modify the `newTree` parameter
*
* @param oldTree
* @param newTree
*/
export var migrateTreeState = function (oldTree, newTree) {
var _a, _b;
var rootId = getTreeRootId(newTree);
if (!rootId)
return;
var nodeStack = [{ id: rootId, isHidden: false }];
var _loop_2 = function () {
var _c = nodeStack.pop(), id = _c.id, isHidden = _c.isHidden;
var node = newTree.get(id);
var isOpen = (_b = (_a = oldTree.get(id)) === null || _a === void 0 ? void 0 : _a.isOpen) !== null && _b !== void 0 ? _b : node.isOpen;
newTree.set(node.id, __assign(__assign({}, node), { isOpen: isOpen, isHidden: isHidden }));
nodeStack.push.apply(nodeStack, node.children.map(function (id) { return ({ id: id, isHidden: node.isHidden || !isOpen }); }));
};
while (nodeStack.length) {
_loop_2();
}
};
//# sourceMappingURL=Tree.utils.js.map