UNPKG

@stanfordbdhg/react-sortable-tree

Version:

Drag-and-drop sortable component for nested data and hierarchies

958 lines (944 loc) 84.5 kB
import React, { Component, Children, cloneElement } from 'react'; import isEqual from 'lodash.isequal'; import { DragSource, DropTarget, DndContext, DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { VList } from 'virtua'; const defaultGetNodeKey = ({ treeIndex }) => treeIndex; // Cheap hack to get the text of a react object const getReactElementText = (parent) => { if (typeof parent === 'string') { return parent; } if (parent === undefined || typeof parent !== 'object' || !parent.props || !parent.props.children || (typeof parent.props.children !== 'string' && typeof parent.props.children !== 'object')) { return ''; } if (typeof parent.props.children === 'string') { return parent.props.children; } return parent.props.children .map((child) => getReactElementText(child)) .join(''); }; // Search for a query string inside a node property const stringSearch = (key, searchQuery, node, path, treeIndex) => { if (typeof node[key] === 'function') { // Search within text after calling its function to generate the text return String(node[key]({ node, path, treeIndex })).includes(searchQuery); } if (typeof node[key] === 'object') { // Search within text inside react elements return getReactElementText(node[key]).includes(searchQuery); } // Search within string return node[key] && String(node[key]).includes(searchQuery); }; const defaultSearchMethod = ({ node, path, treeIndex, searchQuery, }) => { return (stringSearch('title', searchQuery, node, path, treeIndex) || stringSearch('subtitle', searchQuery, node, path, treeIndex)); }; // @ts-nocheck /** * Performs a depth-first traversal over all of the node descendants, * incrementing currentIndex by 1 for each */ const getNodeDataAtTreeIndexOrNextIndex = ({ targetIndex, node, currentIndex, getNodeKey, path = [], lowerSiblingCounts = [], ignoreCollapsed = true, isPseudoRoot = false, }) => { // The pseudo-root is not considered in the path const selfPath = isPseudoRoot ? [] : [...path, getNodeKey({ node, treeIndex: currentIndex })]; // Return target node when found if (currentIndex === targetIndex) { return { node, lowerSiblingCounts, path: selfPath, }; } // Add one and continue for nodes with no children or hidden children if (!node?.children || (ignoreCollapsed && node?.expanded !== true)) { return { nextIndex: currentIndex + 1 }; } // Iterate over each child and their descendants and return the // target node if childIndex reaches the targetIndex let childIndex = currentIndex + 1; const childCount = node.children.length; for (let i = 0; i < childCount; i += 1) { const result = getNodeDataAtTreeIndexOrNextIndex({ ignoreCollapsed, getNodeKey, targetIndex, node: node.children[i], currentIndex: childIndex, lowerSiblingCounts: [...lowerSiblingCounts, childCount - i - 1], path: selfPath, }); if (result.node) { return result; } childIndex = result.nextIndex; } // If the target node is not found, return the farthest traversed index return { nextIndex: childIndex }; }; const getDescendantCount = ({ node, ignoreCollapsed = true, }) => { return (getNodeDataAtTreeIndexOrNextIndex({ getNodeKey: () => { }, ignoreCollapsed, node, currentIndex: 0, targetIndex: -1, }).nextIndex - 1); }; const walkDescendants = ({ callback, getNodeKey, ignoreCollapsed, isPseudoRoot = false, node, parentNode = undefined, currentIndex, path = [], lowerSiblingCounts = [], }) => { // The pseudo-root is not considered in the path const selfPath = isPseudoRoot ? [] : [...path, getNodeKey({ node, treeIndex: currentIndex })]; const selfInfo = isPseudoRoot ? undefined : { node, parentNode, path: selfPath, lowerSiblingCounts, treeIndex: currentIndex, }; if (!isPseudoRoot) { const callbackResult = callback(selfInfo); // Cut walk short if the callback returned false if (callbackResult === false) { return false; } } // Return self on nodes with no children or hidden children if (!node.children || (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) { return currentIndex; } // Get all descendants let childIndex = currentIndex; const childCount = node.children.length; if (typeof node.children !== 'function') { for (let i = 0; i < childCount; i += 1) { childIndex = walkDescendants({ callback, getNodeKey, ignoreCollapsed, node: node.children[i], parentNode: isPseudoRoot ? undefined : node, currentIndex: childIndex + 1, lowerSiblingCounts: [...lowerSiblingCounts, childCount - i - 1], path: selfPath, }); // Cut walk short if the callback returned false if (childIndex === false) { return false; } } } return childIndex; }; const mapDescendants = ({ callback, getNodeKey, ignoreCollapsed, isPseudoRoot = false, node, parentNode = undefined, currentIndex, path = [], lowerSiblingCounts = [], }) => { const nextNode = { ...node }; // The pseudo-root is not considered in the path const selfPath = isPseudoRoot ? [] : [...path, getNodeKey({ node: nextNode, treeIndex: currentIndex })]; const selfInfo = { node: nextNode, parentNode, path: selfPath, lowerSiblingCounts, treeIndex: currentIndex, }; // Return self on nodes with no children or hidden children if (!nextNode.children || (nextNode.expanded !== true && ignoreCollapsed && !isPseudoRoot)) { return { treeIndex: currentIndex, node: callback(selfInfo), }; } // Get all descendants let childIndex = currentIndex; const childCount = nextNode.children.length; if (typeof nextNode.children !== 'function') { nextNode.children = nextNode.children.map((child, i) => { const mapResult = mapDescendants({ callback, getNodeKey, ignoreCollapsed, node: child, parentNode: isPseudoRoot ? undefined : nextNode, currentIndex: childIndex + 1, lowerSiblingCounts: [...lowerSiblingCounts, childCount - i - 1], path: selfPath, }); childIndex = mapResult.treeIndex; return mapResult.node; }); } return { node: callback(selfInfo), treeIndex: childIndex, }; }; const getVisibleNodeCount = ({ treeData }) => { const traverse = (node) => { if (!node.children || node.expanded !== true || typeof node.children === 'function') { return 1; } return (1 + node.children.reduce((total, currentNode) => total + traverse(currentNode), 0)); }; return treeData.reduce((total, currentNode) => total + traverse(currentNode), 0); }; const getVisibleNodeInfoAtIndex = ({ treeData, index: targetIndex, getNodeKey, }) => { if (!treeData || treeData.length === 0) { return undefined; } // Call the tree traversal with a pseudo-root node const result = getNodeDataAtTreeIndexOrNextIndex({ targetIndex, getNodeKey, node: { children: treeData, expanded: true, }, currentIndex: -1, path: [], lowerSiblingCounts: [], isPseudoRoot: true, }); if (result.node) { return result; } return undefined; }; const walk = ({ treeData, getNodeKey, callback, ignoreCollapsed = true, }) => { if (!treeData || treeData.length === 0) { return; } walkDescendants({ callback, getNodeKey, ignoreCollapsed, isPseudoRoot: true, node: { children: treeData }, currentIndex: -1, path: [], lowerSiblingCounts: [], }); }; const map = ({ treeData, getNodeKey, callback, ignoreCollapsed = true, }) => { if (!treeData || treeData.length === 0) { return []; } return mapDescendants({ callback, getNodeKey, ignoreCollapsed, isPseudoRoot: true, node: { children: treeData }, currentIndex: -1, path: [], lowerSiblingCounts: [], }).node.children; }; const toggleExpandedForAll = ({ treeData, expanded = true, }) => { return map({ treeData, callback: ({ node }) => ({ ...node, expanded }), getNodeKey: ({ treeIndex }) => treeIndex, ignoreCollapsed: false, }); }; const changeNodeAtPath = ({ treeData, path, newNode, getNodeKey, ignoreCollapsed = true, }) => { const RESULT_MISS = 'RESULT_MISS'; const traverse = ({ isPseudoRoot = false, node, currentTreeIndex, pathIndex, }) => { if (!isPseudoRoot && getNodeKey({ node, treeIndex: currentTreeIndex }) !== path[pathIndex]) { return RESULT_MISS; } if (pathIndex >= path.length - 1) { // If this is the final location in the path, return its changed form return typeof newNode === 'function' ? newNode({ node, treeIndex: currentTreeIndex }) : newNode; } if (!node.children) { // If this node is part of the path, but has no children, return the unchanged node throw new Error('Path referenced children of node with no children.'); } let nextTreeIndex = currentTreeIndex + 1; for (let i = 0; i < node.children.length; i += 1) { const result = traverse({ node: node.children[i], currentTreeIndex: nextTreeIndex, pathIndex: pathIndex + 1, }); // If the result went down the correct path if (result !== RESULT_MISS) { if (result) { // If the result was truthy (in this case, an object), // pass it to the next level of recursion up return { ...node, children: [ ...node.children.slice(0, i), result, ...node.children.slice(i + 1), ], }; } // If the result was falsy (returned from the newNode function), then // delete the node from the array. return { ...node, children: [ ...node.children.slice(0, i), ...node.children.slice(i + 1), ], }; } nextTreeIndex += 1 + getDescendantCount({ node: node.children[i], ignoreCollapsed }); } return RESULT_MISS; }; // Use a pseudo-root node in the beginning traversal const result = traverse({ node: { children: treeData }, currentTreeIndex: -1, pathIndex: -1, isPseudoRoot: true, }); if (result === RESULT_MISS) { throw new Error('No node found at the given path.'); } return result.children; }; const removeNodeAtPath = ({ treeData, path, getNodeKey, ignoreCollapsed = true, }) => { return changeNodeAtPath({ treeData, path, getNodeKey, ignoreCollapsed, newNode: undefined, // Delete the node }); }; const removeNode = ({ treeData, path, getNodeKey, ignoreCollapsed = true, }) => { let removedNode; let removedTreeIndex; const nextTreeData = changeNodeAtPath({ treeData, path, getNodeKey, ignoreCollapsed, newNode: ({ node, treeIndex }) => { // Store the target node and delete it from the tree removedNode = node; removedTreeIndex = treeIndex; return undefined; }, }); return { treeData: nextTreeData, node: removedNode, treeIndex: removedTreeIndex, }; }; const getNodeAtPath = ({ treeData, path, getNodeKey, ignoreCollapsed = true, }) => { let foundNodeInfo; try { changeNodeAtPath({ treeData, path, getNodeKey, ignoreCollapsed, newNode: ({ node, treeIndex }) => { foundNodeInfo = { node, treeIndex }; return node; }, }); } catch { // Ignore the error -- the null return will be explanation enough } return foundNodeInfo; }; const addNodeUnderParent = ({ treeData, newNode, parentKey = undefined, getNodeKey, ignoreCollapsed = true, expandParent = false, addAsFirstChild = false, }) => { if (parentKey === null || parentKey === undefined) { return addAsFirstChild ? { treeData: [newNode, ...(treeData || [])], treeIndex: 0, } : { treeData: [...(treeData || []), newNode], treeIndex: (treeData || []).length, }; } let insertedTreeIndex; let hasBeenAdded = false; const changedTreeData = map({ treeData, getNodeKey, ignoreCollapsed, callback: ({ node, treeIndex, path }) => { const key = path ? path.at(-1) : undefined; // Return nodes that are not the parent as-is if (hasBeenAdded || key !== parentKey) { return node; } hasBeenAdded = true; const parentNode = { ...node, }; if (expandParent) { parentNode.expanded = true; } // If no children exist yet, just add the single newNode if (!parentNode.children) { insertedTreeIndex = treeIndex + 1; return { ...parentNode, children: [newNode], }; } if (typeof parentNode.children === 'function') { throw new TypeError('Cannot add to children defined by a function'); } let nextTreeIndex = treeIndex + 1; for (let i = 0; i < parentNode.children.length; i += 1) { nextTreeIndex += 1 + getDescendantCount({ node: parentNode.children[i], ignoreCollapsed }); } insertedTreeIndex = nextTreeIndex; const children = addAsFirstChild ? [newNode, ...parentNode.children] : [...parentNode.children, newNode]; return { ...parentNode, children, }; }, }); if (!hasBeenAdded) { throw new Error('No node found with the given key.'); } return { treeData: changedTreeData, treeIndex: insertedTreeIndex, }; }; const addNodeAtDepthAndIndex = ({ targetDepth, minimumTreeIndex, newNode, ignoreCollapsed, expandParent, isPseudoRoot = false, isLastChild, node, currentIndex, currentDepth, getNodeKey, path = [], }) => { const selfPath = (n) => isPseudoRoot ? [] : [...path, getNodeKey({ node: n, treeIndex: currentIndex })]; // If the current position is the only possible place to add, add it if (currentIndex >= minimumTreeIndex - 1 || (isLastChild && !(node.children && node.children.length > 0))) { if (typeof node.children === 'function') { throw new TypeError('Cannot add to children defined by a function'); } else { const extraNodeProps = expandParent ? { expanded: true } : {}; const nextNode = { ...node, ...extraNodeProps, children: node.children ? [newNode, ...node.children] : [newNode], }; return { node: nextNode, nextIndex: currentIndex + 2, insertedTreeIndex: currentIndex + 1, parentPath: selfPath(nextNode), parentNode: isPseudoRoot ? undefined : nextNode, }; } } // If this is the target depth for the insertion, // i.e., where the newNode can be added to the current node's children if (currentDepth >= targetDepth - 1) { // Skip over nodes with no children or hidden children if (!node.children || typeof node.children === 'function' || (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) { return { node, nextIndex: currentIndex + 1 }; } // Scan over the children to see if there's a place among them that fulfills // the minimumTreeIndex requirement let childIndex = currentIndex + 1; let insertedTreeIndex; let insertIndex; for (let i = 0; i < node.children.length; i += 1) { // If a valid location is found, mark it as the insertion location and // break out of the loop if (childIndex >= minimumTreeIndex) { insertedTreeIndex = childIndex; insertIndex = i; break; } // Increment the index by the child itself plus the number of descendants it has childIndex += 1 + getDescendantCount({ node: node.children[i], ignoreCollapsed }); } // If no valid indices to add the node were found if (insertIndex === null || insertIndex === undefined) { // If the last position in this node's children is less than the minimum index // and there are more children on the level of this node, return without insertion if (childIndex < minimumTreeIndex && !isLastChild) { return { node, nextIndex: childIndex }; } // Use the last position in the children array to insert the newNode insertedTreeIndex = childIndex; insertIndex = node.children.length; } // Insert the newNode at the insertIndex const nextNode = { ...node, children: [ ...node.children.slice(0, insertIndex), newNode, ...node.children.slice(insertIndex), ], }; // Return node with successful insert result return { node: nextNode, nextIndex: childIndex, insertedTreeIndex, parentPath: selfPath(nextNode), parentNode: isPseudoRoot ? undefined : nextNode, }; } // Skip over nodes with no children or hidden children if (!node.children || typeof node.children === 'function' || (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) { return { node, nextIndex: currentIndex + 1 }; } // Get all descendants let insertedTreeIndex; let pathFragment; let parentNode; let childIndex = currentIndex + 1; let newChildren = node.children; if (typeof newChildren !== 'function') { newChildren = newChildren.map((child, i) => { if (insertedTreeIndex !== null && insertedTreeIndex !== undefined) { return child; } const mapResult = addNodeAtDepthAndIndex({ targetDepth, minimumTreeIndex, newNode, ignoreCollapsed, expandParent, isLastChild: isLastChild && i === newChildren.length - 1, node: child, currentIndex: childIndex, currentDepth: currentDepth + 1, getNodeKey, path: [], // Cannot determine the parent path until the children have been processed }); if ('insertedTreeIndex' in mapResult) { ({ insertedTreeIndex, parentNode, parentPath: pathFragment, } = mapResult); } childIndex = mapResult.nextIndex; return mapResult.node; }); } const nextNode = { ...node, children: newChildren }; const result = { node: nextNode, nextIndex: childIndex, }; if (insertedTreeIndex !== null && insertedTreeIndex !== undefined) { result.insertedTreeIndex = insertedTreeIndex; result.parentPath = [...selfPath(nextNode), ...pathFragment]; result.parentNode = parentNode; } return result; }; const insertNode = ({ treeData, depth: targetDepth, minimumTreeIndex, newNode, getNodeKey, ignoreCollapsed = true, expandParent = false, }) => { if (!treeData && targetDepth === 0) { return { treeData: [newNode], treeIndex: 0, path: [getNodeKey({ node: newNode, treeIndex: 0 })], parentNode: undefined, }; } const insertResult = addNodeAtDepthAndIndex({ targetDepth, minimumTreeIndex, newNode, ignoreCollapsed, expandParent, getNodeKey, isPseudoRoot: true, isLastChild: true, node: { children: treeData }, currentIndex: -1, currentDepth: -1, }); if (!('insertedTreeIndex' in insertResult)) { throw new Error('No suitable position found to insert.'); } const treeIndex = insertResult.insertedTreeIndex; return { treeData: insertResult.node.children, treeIndex, path: [ ...insertResult.parentPath, getNodeKey({ node: newNode, treeIndex }), ], parentNode: insertResult.parentNode, }; }; const getFlatDataFromTree = ({ treeData, getNodeKey, ignoreCollapsed = true, }) => { if (!treeData || treeData.length === 0) { return []; } const flattened = []; walk({ treeData, getNodeKey, ignoreCollapsed, callback: (nodeInfo) => { flattened.push(nodeInfo); }, }); return flattened; }; const getTreeFromFlatData = ({ flatData, getKey = (node) => node.id, getParentKey = (node) => node.parentId, rootKey = '0', }) => { if (!flatData) { return []; } const childrenToParents = {}; for (const child of flatData) { const parentKey = getParentKey(child); if (parentKey in childrenToParents) { childrenToParents[parentKey].push(child); } else { childrenToParents[parentKey] = [child]; } } if (!(rootKey in childrenToParents)) { return []; } const trav = (parent) => { const parentKey = getKey(parent); if (parentKey in childrenToParents) { return { ...parent, children: childrenToParents[parentKey].map((child) => trav(child)), }; } return { ...parent }; }; return childrenToParents[rootKey].map((child) => trav(child)); }; const isDescendant = (older, younger) => { return (!!older.children && typeof older.children !== 'function' && older.children.some((child) => child === younger || isDescendant(child, younger))); }; const getDepth = (node, depth = 0) => { if (!node.children) { return depth; } if (typeof node.children === 'function') { return depth + 1; } return node.children.reduce((deepest, child) => Math.max(deepest, getDepth(child, depth + 1)), depth); }; const find = ({ getNodeKey, treeData, searchQuery, searchMethod, searchFocusOffset, expandAllMatchPaths = false, expandFocusMatchPaths = true, }) => { let matchCount = 0; const trav = ({ isPseudoRoot = false, node, currentIndex, path = [] }) => { let matches = []; let isSelfMatch = false; let hasFocusMatch = false; // The pseudo-root is not considered in the path const selfPath = isPseudoRoot ? [] : [...path, getNodeKey({ node, treeIndex: currentIndex })]; const extraInfo = isPseudoRoot ? undefined : { path: selfPath, treeIndex: currentIndex, }; // Nodes with with children that aren't lazy const hasChildren = node.children && typeof node.children !== 'function' && node.children.length > 0; // Examine the current node to see if it is a match if (!isPseudoRoot && searchMethod({ ...extraInfo, node, searchQuery })) { if (matchCount === searchFocusOffset) { hasFocusMatch = true; } // Keep track of the number of matching nodes, so we know when the searchFocusOffset // is reached matchCount += 1; // We cannot add this node to the matches right away, as it may be changed // during the search of the descendants. The entire node is used in // comparisons between nodes inside the `matches` and `treeData` results // of this method (`find`) isSelfMatch = true; } let childIndex = currentIndex; const newNode = { ...node }; if (hasChildren) { // Get all descendants newNode.children = newNode.children.map((child) => { const mapResult = trav({ node: child, currentIndex: childIndex + 1, path: selfPath, }); // Ignore hidden nodes by only advancing the index counter to the returned treeIndex // if the child is expanded. // // The child could have been expanded from the start, // or expanded due to a matching node being found in its descendants if (mapResult.node.expanded) { childIndex = mapResult.treeIndex; } else { childIndex += 1; } if (mapResult.matches.length > 0 || mapResult.hasFocusMatch) { matches = [...matches, ...mapResult.matches]; if (mapResult.hasFocusMatch) { hasFocusMatch = true; } // Expand the current node if it has descendants matching the search // and the settings are set to do so. if ((expandAllMatchPaths && mapResult.matches.length > 0) || ((expandAllMatchPaths || expandFocusMatchPaths) && mapResult.hasFocusMatch)) { newNode.expanded = true; } } return mapResult.node; }); } // Cannot assign a treeIndex to hidden nodes if (!isPseudoRoot && !newNode.expanded) { matches = matches.map((match) => ({ ...match, treeIndex: undefined, })); } // Add this node to the matches if it fits the search criteria. // This is performed at the last minute so newNode can be sent in its final form. if (isSelfMatch) { matches = [{ ...extraInfo, node: newNode }, ...matches]; } return { node: matches.length > 0 ? newNode : node, matches, hasFocusMatch, treeIndex: childIndex, }; }; const result = trav({ node: { children: treeData }, isPseudoRoot: true, currentIndex: -1, }); return { matches: result.matches, treeData: result.node.children, }; }; // very simple className utility for creating a classname string... // Falsy arguments are ignored: // // const active = true // const className = classnames( // "class1", // !active && "class2", // active && "class3" // ); // returns -> class1 class3"; // // Use Boolean constructor as a filter callback // Allows for loose type truthy/falsey checks // Boolean("") === false; // Boolean(false) === false; // Boolean(undefined) === false; // Boolean(null) === false; // Boolean(0) === false; // Boolean("classname") === true; const classnames = (...classes) => classes.filter(Boolean).join(' '); function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z$2 = ".rst__rowWrapper {\n padding: 10px 10px 10px 0;\n height: 100%;\n box-sizing: border-box;\n}\n\n.rst__rtl.rst__rowWrapper {\n padding: 10px 0 10px 10px;\n}\n\n.rst__row {\n height: 100%;\n white-space: nowrap;\n display: flex;\n}\n.rst__row > * {\n box-sizing: border-box;\n}\n\n/**\n * The outline of where the element will go if dropped, displayed while dragging\n */\n.rst__rowLandingPad,\n.rst__rowCancelPad {\n border: none !important;\n box-shadow: none !important;\n outline: none !important;\n}\n.rst__rowLandingPad > *,\n.rst__rowCancelPad > * {\n opacity: 0 !important;\n}\n.rst__rowLandingPad::before,\n.rst__rowCancelPad::before {\n background-color: lightblue;\n border: 3px dashed white;\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: -1;\n}\n\n/**\n * Alternate appearance of the landing pad when the dragged location is invalid\n */\n.rst__rowCancelPad::before {\n background-color: #e6a8ad;\n}\n\n/**\n * Nodes matching the search conditions are highlighted\n */\n.rst__rowSearchMatch {\n outline: solid 3px #0080ff;\n}\n\n/**\n * The node that matches the search conditions and is currently focused\n */\n.rst__rowSearchFocus {\n outline: solid 3px #fc6421;\n}\n\n.rst__rowContents,\n.rst__rowLabel,\n.rst__rowToolbar,\n.rst__moveHandle,\n.rst__toolbarButton {\n display: inline-block;\n vertical-align: middle;\n}\n\n.rst__rowContents {\n position: relative;\n height: 100%;\n border: solid #bbb 1px;\n border-left: none;\n box-shadow: 0 2px 2px -2px;\n padding: 0 5px 0 10px;\n border-radius: 2px;\n min-width: 230px;\n flex: 1 0 auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.rst__rtl.rst__rowContents {\n border-right: none;\n border-left: solid #bbb 1px;\n padding: 0 10px 0 5px;\n}\n\n.rst__rowContentsDragDisabled {\n border-left: solid #bbb 1px;\n}\n\n.rst__rtl.rst__rowContentsDragDisabled {\n border-right: solid #bbb 1px;\n border-left: solid #bbb 1px;\n}\n\n.rst__rowLabel {\n flex: 0 1 auto;\n padding-right: 20px;\n}\n.rst__rtl.rst__rowLabel {\n padding-left: 20px;\n padding-right: inherit;\n}\n\n.rst__rowToolbar {\n flex: 0 1 auto;\n display: flex;\n}\n\n.rst__moveHandle,\n.rst__loadingHandle {\n height: 100%;\n width: 44px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MiIgaGVpZ2h0PSI0MiI+PGcgc3Ryb2tlPSIjRkZGIiBzdHJva2Utd2lkdGg9IjIuOSIgPjxwYXRoIGQ9Ik0xNCAxNS43aDE0LjQiLz48cGF0aCBkPSJNMTQgMjEuNGgxNC40Ii8+PHBhdGggZD0iTTE0IDI3LjFoMTQuNCIvPjwvZz4KPC9zdmc+');\n background-color: #6DB3F2;\n background-position: center;\n border: solid #aaa 1px;\n box-shadow: 0 2px 2px -2px;\n cursor: move;\n border-radius: 1px;\n z-index: 1;\n}\n\n.rst__loadingHandle {\n cursor: default;\n background: #d9d9d9;\n}\n\n@keyframes pointFade {\n 0%,\n 19.999%,\n 100% {\n opacity: 0;\n }\n 20% {\n opacity: 1;\n }\n}\n\n.rst__loadingCircle {\n width: 80%;\n height: 80%;\n margin: 10%;\n position: relative;\n}\n\n.rst__loadingCirclePoint {\n width: 100%;\n height: 100%;\n position: absolute;\n left: 0;\n top: 0;\n}\n\n.rst__rtl.rst__loadingCirclePoint {\n right: 0;\n left: initial;\n}\n\n.rst__loadingCirclePoint::before {\n content: '';\n display: block;\n margin: 0 auto;\n width: 11%;\n height: 30%;\n background-color: #fff;\n border-radius: 30%;\n animation: pointFade 800ms infinite ease-in-out both;\n}\n.rst__loadingCirclePoint:nth-of-type(1) {\n transform: rotate(0deg);\n}\n.rst__loadingCirclePoint:nth-of-type(7) {\n transform: rotate(180deg);\n}\n.rst__loadingCirclePoint:nth-of-type(1)::before,\n.rst__loadingCirclePoint:nth-of-type(7)::before {\n animation-delay: -800ms;\n}\n.rst__loadingCirclePoint:nth-of-type(2) {\n transform: rotate(30deg);\n}\n.rst__loadingCirclePoint:nth-of-type(8) {\n transform: rotate(210deg);\n}\n.rst__loadingCirclePoint:nth-of-type(2)::before,\n.rst__loadingCirclePoint:nth-of-type(8)::before {\n animation-delay: -666ms;\n}\n.rst__loadingCirclePoint:nth-of-type(3) {\n transform: rotate(60deg);\n}\n.rst__loadingCirclePoint:nth-of-type(9) {\n transform: rotate(240deg);\n}\n.rst__loadingCirclePoint:nth-of-type(3)::before,\n.rst__loadingCirclePoint:nth-of-type(9)::before {\n animation-delay: -533ms;\n}\n.rst__loadingCirclePoint:nth-of-type(4) {\n transform: rotate(90deg);\n}\n.rst__loadingCirclePoint:nth-of-type(10) {\n transform: rotate(270deg);\n}\n.rst__loadingCirclePoint:nth-of-type(4)::before,\n.rst__loadingCirclePoint:nth-of-type(10)::before {\n animation-delay: -400ms;\n}\n.rst__loadingCirclePoint:nth-of-type(5) {\n transform: rotate(120deg);\n}\n.rst__loadingCirclePoint:nth-of-type(11) {\n transform: rotate(300deg);\n}\n.rst__loadingCirclePoint:nth-of-type(5)::before,\n.rst__loadingCirclePoint:nth-of-type(11)::before {\n animation-delay: -266ms;\n}\n.rst__loadingCirclePoint:nth-of-type(6) {\n transform: rotate(150deg);\n}\n.rst__loadingCirclePoint:nth-of-type(12) {\n transform: rotate(330deg);\n}\n.rst__loadingCirclePoint:nth-of-type(6)::before,\n.rst__loadingCirclePoint:nth-of-type(12)::before {\n animation-delay: -133ms;\n}\n.rst__loadingCirclePoint:nth-of-type(7) {\n transform: rotate(180deg);\n}\n.rst__loadingCirclePoint:nth-of-type(13) {\n transform: rotate(360deg);\n}\n.rst__loadingCirclePoint:nth-of-type(7)::before,\n.rst__loadingCirclePoint:nth-of-type(13)::before {\n animation-delay: 0ms;\n}\n\n.rst__rowTitle {\n font-weight: bold;\n}\n\n.rst__rowTitleWithSubtitle {\n font-size: 85%;\n display: block;\n height: 0.8rem;\n}\n\n.rst__rowSubtitle {\n font-size: 70%;\n line-height: 1;\n}\n\n.rst__collapseButton,\n.rst__expandButton {\n appearance: none;\n border: none;\n position: absolute;\n border-radius: 100%;\n box-shadow: 0 0 0 1px #000;\n width: 16px;\n height: 16px;\n padding: 0;\n top: 50%;\n transform: translate(-50%, -50%);\n cursor: pointer;\n}\n.rst__rtl.rst__collapseButton,\n.rst__rtl.rst__expandButton {\n transform: translate(50%, -50%);\n}\n.rst__collapseButton:focus,\n.rst__expandButton:focus {\n outline: none;\n box-shadow: 0 0 0 1px #000, 0 0 1px 3px #83bef9;\n}\n.rst__collapseButton:hover:not(:active),\n.rst__expandButton:hover:not(:active) {\n background-size: 24px;\n height: 20px;\n width: 20px;\n}\n\n.rst__collapseButton {\n background: #fff\n url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48L2c+Cjwvc3ZnPg==')\n no-repeat center;\n}\n\n.rst__expandButton {\n background: #fff\n url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48cGF0aCBkPSJNOSA0LjV2OSIvPjwvZz4KPC9zdmc+')\n no-repeat center;\n}\n\n/**\n * Line for under a node with children\n */\n.rst__lineChildren {\n height: 100%;\n display: inline-block;\n position: absolute;\n}\n.rst__lineChildren::after {\n content: '';\n position: absolute;\n background-color: black;\n width: 1px;\n left: 50%;\n bottom: 0;\n height: 10px;\n}\n\n.rst__rtl.rst__lineChildren::after {\n right: 50%;\n left: initial;\n}\n"; styleInject(css_248z$2); const defaultProps$3 = { isSearchMatch: false, isSearchFocus: false, canDrag: false, toggleChildrenVisibility: undefined, buttons: [], className: '', style: {}, parentNode: undefined, draggedNode: undefined, canDrop: false, title: undefined, subtitle: undefined, rowDirection: 'ltr', }; const NodeRendererDefault = (props) => { props = { ...defaultProps$3, ...props }; const { scaffoldBlockPxWidth, toggleChildrenVisibility, connectDragPreview, connectDragSource, isDragging, canDrop, canDrag, node, title, subtitle, draggedNode, path, treeIndex, isSearchMatch, isSearchFocus, buttons, className, style, didDrop, treeId: _treeId, isOver: _isOver, // Not needed, but preserved for other renderers parentNode: _parentNode, // Needed for dndManager rowDirection, ...otherProps } = props; const nodeTitle = title || node.title; const nodeSubtitle = subtitle || node.subtitle; const rowDirectionClass = rowDirection === 'rtl' ? 'rst__rtl' : undefined; let handle; if (canDrag) { handle = typeof node.children === 'function' && node.expanded ? (React.createElement("div", { className: "rst__loadingHandle" }, React.createElement("div", { className: "rst__loadingCircle" }, Array.from({ length: 12 }).map((_, index) => (React.createElement("div", { key: index, className: classnames('rst__loadingCirclePoint', rowDirectionClass ?? '') })))))) : (connectDragSource(React.createElement("div", { className: "rst__moveHandle" }), { dropEffect: 'copy', })); } const isDraggedDescendant = draggedNode && isDescendant(draggedNode, node); const isLandingPadActive = !didDrop && isDragging; let buttonStyle = { left: -0.5 * scaffoldBlockPxWidth, right: 0 }; if (rowDirection === 'rtl') { buttonStyle = { right: -0.5 * scaffoldBlockPxWidth, left: 0 }; } return (React.createElement("div", { style: { height: '100%' }, ...otherProps }, toggleChildrenVisibility && node.children && (node.children.length > 0 || typeof node.children === 'function') && (React.createElement("div", null, React.createElement("button", { type: "button", "aria-label": node.expanded ? 'Collapse' : 'Expand', className: classnames(node.expanded ? 'rst__collapseButton' : 'rst__expandButton', rowDirectionClass ?? ''), style: buttonStyle, onClick: () => toggleChildrenVisibility({ node, path, treeIndex, }) }), node.expanded && !isDragging && (React.createElement("div", { style: { width: scaffoldBlockPxWidth }, className: classnames('rst__lineChildren', rowDirectionClass ?? '') })))), React.createElement("div", { className: classnames('rst__rowWrapper', rowDirectionClass ?? '') }, connectDragPreview(React.createElement("div", { className: classnames('rst__row', isLandingPadActive ? 'rst__rowLandingPad' : '', isLandingPadActive && !canDrop ? 'rst__rowCancelPad' : '', isSearchMatch ? 'rst__rowSearchMatch' : '', isSearchFocus ? 'rst__rowSearchFocus' : '', rowDirectionClass ?? '', className ?? ''), style: { opacity: isDraggedDescendant ? 0.5 : 1, ...style, } }, handle, React.createElement("div", { className: classnames('rst__rowContents', canDrag ? '' : 'rst__rowContentsDragDisabled', rowDirectionClass ?? '') }, React.createElement("div", { className: classnames('rst__rowLabel', rowDirectionClass ?? '') }, React.createElement("span", { className: classnames('rst__rowTitle', node.subtitle ? 'rst__rowTitleWithSubtitle' : '') }, typeof nodeTitle === 'function' ? nodeTitle({ node, path, treeIndex, }) : nodeTitle), nodeSubtitle && (React.createElement("span", { className: "rst__rowSubtitle" }, typeof nodeSubtitle === 'function' ? nodeSubtitle({ node, path, treeIndex, }) : nodeSubtitle))), React.createElement("div", { className: "rst__rowToolbar" }, buttons?.map((btn, index) => (React.createElement("div", { key: index, className: "rst__toolbarButton" }, btn)))))))))); }; var css_248z$1 = ".rst__placeholder {\n position: relative;\n height: 68px;\n max-width: 300px;\n padding: 10px;\n}\n.rst__placeholder,\n.rst__placeholder > * {\n box-sizing: border-box;\n}\n.rst__placeholder::before {\n border: 3px dashed #d9d9d9;\n content: '';\n position: absolute;\n top: 5px;\n right: 5px;\n bottom: 5px;\n left: 5px;\n z-index: -1;\n}\n\n/**\n * The outline of where the element will go if dropped, displayed while dragging\n */\n.rst__placeholderLandingPad,\n.rst__placeholderCancelPad {\n border: none !important;\n box-shadow: none !important;\n outline: none !important;\n}\n.rst__placeholderLandingPad *,\n.rst__placeholderCancelPad * {\n opacity: 0 !important;\n}\n.rst__placeholderLandingPad::before,\n.rst__placeholderCancelPad::before {\n background-color: lightblue;\n border-color: white;\n}\n\n/**\n * Alternate appearance of the landing pad when the dragged location is invalid\n */\n.rst__placeholderCancelPad::before {\n background-color: #e6a8ad;\n}\n"; styleInject(css_248z$1); const defaultProps$2 = { isOver: false, canDrop: false, }; const PlaceholderRendererDefault = (props) => { props = { ...defaultProps$2, ...props }; const { canDrop, isOver } = props; return (React.createElement("div", { className: classnames('rst__placeholder', canDrop ? 'rst__placeholderLandingPad' : '', canDrop && !isOver ? 'rst__placeholderCancelPad' : '') })); }; var css_248z = ".rst__node {\n min-width: 100%;\n white-space: nowrap;\n position: relative;\n text-align: left;\n height: 62px;\n}\n\n.rst__node.rst__rtl {\n text-align: right;\n}\n\n.rst__nodeContent {\n position: absolute;\n top: 0;\n bottom: 0;\n}\n\n/* ==========================================================================\n Scaffold\n\n Line-overlaid blocks used for showing the tree structure\n ========================================================================== */\n.rst__lineBlock,\n.rst__absoluteLineBlock {\n height: 100%;\n position: relative;\n display: inline-block;\n}\n\n.rst__absoluteLineBlock {\n position: absolute;\n top: 0;\n}\n\n.rst__lineHalfHorizontalRight::before,\n.rst__lineFullVertical::after,\n.rst__lineHalfVerticalTop::after,\n.rst__lineHalfVerticalBottom::after {\n position: absolute;\n content: '';\n background-color: black;\n}\n\n/**\n * +-----+\n * | |\n * | +--+\n * | |\n * +-----+\n */\n.rst__lineHalfHorizontalRight::before {\n height: 1px;\n top: 50%;\n right: 0;\n width: 50%;\n}\n\n.rst__rtl.rst__lineHalfHorizontalRight::before {\n left: 0;\n right: initial;\n}\n\n/**\n * +--+--+\n * | | |\n * | | |\n * | | |\n * +--+--+\n */\n.rst__lineFullVertical::after,\n.rst__lineHalfVerticalTop::after,\n.rst__lineHalfVerticalBottom::after {\n width: 1px;\n left: 50%;\n top: 0;\n height: 100%;\n}\n\n/**\n * +--+--+\n * | | |\n * | | |\n * | | |\n * +--+--+\n */\n.rst__rtl.rst__lineFullVertical::after,\n.rst__rtl.rst__lineHalfVerticalTop::after,\n.rst__rtl.rst__lineHalfVerticalBottom::after {\n right: 50%;\n left: initial;\n}\n\n/**\n * +-----+\n * | | |\n * | + |\n * | |\n * +-----+\n */\n.rst__lineHalfVerticalTop::after {\n height: 50%;\n}\n\n/**\n * +-----+\n * | |\n * | + |\n * | | |\n * +-----+\n */\n.rst__lineHalfVerticalBottom::after {\n top: auto;\n bottom: 0;\n height: 50%;\n}\n\n/* Highlight line for pointing to dragged row destination\n ========================================================================== */\n/**\n * +--+--+\n * | | |\n * | | |\n * | | |\n * +--+--+\n */\n.rst__highlightLineVertical {\n z-index: 3;\n}\n.rst__highlightLineVertical::before {\n position: absolute;\n content: '';\n background-color: #36c2f6;\n width: 8px;\n margin-left: -4px;\n left: 50%;\n top: 0;\n height: 100%;\n}\n\n.rst__rtl.rst__highlightLineVertical::before {\n margin-left: initial;\n margin-right: -4px;\n left: initial;\n right: 50%;\n}\n\n@keyframes arrow-pulse {\n 0% {\n transform: translate(0, 0);\n opacity: 0;\n }\n 30% {\n transform: translate(0, 300%);\n opacity: 1;\n }\n 70% {\n transform: translate(0, 700%);\n opacity: 1;\n }\n 100% {\n transform: translate(0, 1000%);\n opacity: 0;\n }\n}\n.rst__highlightLineVertical::after {\n content: '';\n position: absolute;\n height: 0;\n margin-left: -4px;\n left: 50%;\n top: 0;\n border-left: 4px solid transparent;\n border-right: 4px solid transparent;\n border-top: 4px solid white;\n animation: arrow-pulse 1s infinite linear both;\n}\n\n.rst__rtl.rst__highlightLineVertical::after {\n margin-left: initial;\n margin-right: -4px;\n right: 50%;\n left: initial;\n}\n\n/**\n * +-----+\n * | |\n * | +--+\n * | | |\n * +--+--+\n */\n.rst__highlightTopLeftCorner::before {\n z-index: 3;\n content: '';\n position: absolute;\n border-top: solid 8px #36c2f6;\n border-left: solid 8px #36c2f6;\n box-sizing: border-box;\n height: calc(50% + 4px);\n top: 50%;\n margin-top: -4px;\n right: 0;\n width: calc(50% + 4px);\n}\n\n.rst__rtl.rst__highlightTopLeftCorner::before {\n border-right: solid 8px #36c2f6;\n border-left: none;\n left: 0;\n right: initial;\n}\n\n/**\n * +--+--+\n * | | |\n * | | |\n * | +->|\n * +-----+\n */\n.rst__highlightBottomLeftCorner {\n z-index: 3;\n}\n.rst__highlightBottomLeftCorner::before {\n content: '';\n position: absolute;\n border-bottom: solid 8px #36c2f6;\n border-left: solid 8px #36c2f6;\n box-sizing: border-box;\n height: calc(100% + 4px);\n top: 0;\n right: 12px;\n width: calc(50% - 8px);\n}\n\n.rst__rtl.rst__highlightBottomLeftCorner::before {\n border-right: solid 8px #36c2f6;\n border-left: none;\n left: 12px;\n right: initial;\n}\n\n.rst__highlightBottomLeftCorner::after {\n content: '';\n position: absolute;\n height: 0;\n right: 0;\n top: 100%;\n margin-top: -12px;\n border-top: 12px solid transparent;\n border-bottom: 12px solid transparent;\n border-left: 12px solid #36c2f6;\n}\n\n.rst__rtl.rst__highlightBottomLeftCorner::after {\n left: 0;\n right: initial;\n border-right: 12px solid #36c2f6;\n border-left: none;\n}\n"; styleInject(css_248z); const defaultProps$1 = { swapFrom: undefined, swapDepth: undefined, swapLength: undefined, canDrop: false, draggedNode: undefined, rowDirection: 'ltr', }; class TreeNodeComponent extends Component { render() { const props = { ...defaultProps$1, ...this.props }; const { children, listIndex, swapFrom, swapLength, swapDepth, scaffoldBlockPxWidth, lowerSiblingCounts, connectDropTarget, isOver, draggedNode, canDrop, treeIndex, rowHeight, treeId: _treeId, // Delete from otherProps getPrevRow: _getPrevRow, // Delete from otherProps node: _node, // Delete from otherProps path: _path, // Delete from otherProps rowDirection, ...otherProps } = props; const rowDirectionClass = rowDirection === 'rtl' ? 'rst__rtl' : undefined; // Construct the scaffold representing the structure of the tree const scaffoldBlockCount = lowerSiblingCounts.length; const scaffold = []; for (const [i, lowerSiblingCount] of lowerSiblingCounts.entries()) { let lineClass = ''; if (lowerSiblingCount > 0) { // At this level in the tree, the nodes had sibling nodes further down if (listIndex === 0) { // Top-left corner of the tree // +-----+ // | | // | +--+ // | | | // +--+--+ lineClass = 'rst__lineHalfHorizontalRight rst__lineHalfVerticalBottom'; } else if (i === scaffoldBlockCount - 1) { // Last scaffold block in the row, right before the row content // +--+--+ // | | | // | +--+ // | | | // +--+--+ lineClass = 'rst__lineHalfHorizontalRight rst__lineFullVertical'; } else { // Simply connecting the line extending down to the next sibling on this level // +--+--+ // | | | // | | | // | |