UNPKG

@momentum-ui/react-collaboration

Version:

Cisco Momentum UI Framework for React Collaboration Applications

402 lines 13.9 kB
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