UNPKG

@nosferatu500/react-sortable-tree

Version:

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

928 lines (914 loc) 89.6 kB
import { produce, original } from 'immer'; import { jsx, jsxs } from 'react/jsx-runtime'; import React, { Children, cloneElement, useLayoutEffect, useRef, useCallback, useId, useTransition, useMemo, useDeferredValue, useState, useEffect } from 'react'; import { useDrag, useDrop, DndProvider, DndContext } 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 (typeof parent === 'number') { return String(parent); } const parentEle = parent; if (!parentEle || typeof parentEle !== 'object' || !parentEle.props || !parentEle.props.children) { return ''; } if (typeof parentEle.props.children === 'string') { return parentEle.props.children; } if (Array.isArray(parentEle.props.children)) { return parentEle.props.children .map((child) => getReactElementText(child)) .join(''); } return getReactElementText(parentEle.props.children); }; // 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)) ?? false); }; /** * 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 || typeof node.children === 'function' || (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: () => 0, 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 null; } // Call the tree traversal with a pseudo-root node const result = getNodeDataAtTreeIndexOrNextIndex({ targetIndex, getNodeKey, node: { children: treeData, expanded: true, }, currentIndex: -1, path: [], lowerSiblingCounts: [], ignoreCollapsed: true, isPseudoRoot: true, }); if (result.node) { return result; } return null; }; 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, }) => { if (!treeData || treeData.length === 0) return []; return produce(treeData, (draft) => { let currentNode = { children: draft }; // Pseudo-root let currentTreeIndex = -1; for (const [i, key] of path.entries()) { const isLast = i === path.length - 1; if (!currentNode.children) { throw new Error('Path referenced children of node with no children.'); } // Find the child matching the key let foundIndex = -1; for (let j = 0; j < currentNode.children.length; j++) { const child = currentNode.children[j]; const childIndex = currentTreeIndex + 1; // We need to calculate indices to match keys, but we don't need // to modify the index logic significantly for Immer, just navigation if (getNodeKey({ node: child, treeIndex: childIndex }) === key) { foundIndex = j; currentTreeIndex = childIndex; break; } // Increment index by skipping descendants currentTreeIndex += 1 + getDescendantCount({ node: child, ignoreCollapsed }); } if (foundIndex === -1) { throw new Error('No node found at the given path.'); } if (isLast) { const targetNode = currentNode.children[foundIndex]; const result = typeof newNode === 'function' ? newNode({ node: targetNode, treeIndex: currentTreeIndex }) : newNode; if (result === undefined || result === null) { currentNode.children.splice(foundIndex, 1); } else { currentNode.children[foundIndex] = result; } } else { currentNode = currentNode.children[foundIndex]; } } }); }; 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 }) => { removedNode = original(node) || 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: original(node) || node, treeIndex }; return node; }, }); } catch { // Ignore the error -- the null return will be explanation enough } return foundNodeInfo ?? null; }; const addNodeUnderParent = ({ treeData, newNode, parentKey = undefined, getNodeKey, ignoreCollapsed = true, expandParent = false, addAsFirstChild = false, }) => { if (parentKey === null || parentKey === undefined) { const newTreeData = addAsFirstChild ? [newNode, ...(treeData || [])] : [...(treeData || []), newNode]; return { treeData: newTreeData, treeIndex: addAsFirstChild ? 0 : (treeData || []).length, }; } let insertedTreeIndex = -1; let found = false; const nextTreeData = produce(treeData, (draft) => { // Helper to find parent and insert const findAndInsert = (nodes, currentTreeIndex) => { let indexCounter = currentTreeIndex; for (const node of nodes) { // Calculate key const key = getNodeKey({ node, treeIndex: indexCounter }); if (key === parentKey) { found = true; if (expandParent) { node.expanded = true; } if (!node.children) { node.children = [newNode]; insertedTreeIndex = indexCounter + 1; return -1; // Stop traversal, we found it } if (typeof node.children === 'function') { throw new TypeError('Cannot add to children defined by a function'); } // Calculate where the new node lands in the index let childIndexOffset = indexCounter + 1; if (!addAsFirstChild) { for (const child of node.children) { childIndexOffset += 1 + getDescendantCount({ node: child, ignoreCollapsed }); } } insertedTreeIndex = childIndexOffset; if (addAsFirstChild) { node.children.unshift(newNode); } else { node.children.push(newNode); } return -1; // Stop traversal } // Standard traversal increment // If not found, add self (1) + descendants const descendants = getDescendantCount({ node, ignoreCollapsed }); const nextIndex = indexCounter + 1 + descendants; // If the node has children, we might need to look inside them if (node.children && typeof node.children !== 'function' && (node.expanded || !ignoreCollapsed)) { // If we are strictly counting indices, we can skip traversing children // if we know the key isn't in there? // No, getNodeKey depends on treeIndex, so we must traverse to calculate index accurately. const result = findAndInsert(node.children, indexCounter + 1); if (result === -1) return -1; } indexCounter = nextIndex; } return indexCounter; }; findAndInsert(draft, 0); }); if (!found) { throw new Error('No node found with the given key.'); } return { treeData: nextTreeData, 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.toSpliced(insertIndex, 0, newNode), }; // 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 && mapResult.insertedTreeIndex !== undefined) { ({ 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: null, }; } const insertResult = addNodeAtDepthAndIndex({ targetDepth, minimumTreeIndex, newNode, ignoreCollapsed, expandParent, getNodeKey, isPseudoRoot: true, isLastChild: true, node: { children: treeData }, currentIndex: -1, currentDepth: -1, }); if (!('insertedTreeIndex' in insertResult) || insertResult.insertedTreeIndex === undefined) { 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 ?? null, }; }; 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 = Object.groupBy(flatData, (child) => getParentKey(child)); if (rootKey === null || !childrenToParents[rootKey]) { return []; } const trav = (parent) => { const parentKey = getKey(parent); const children = childrenToParents[parentKey]; if (children) { return { ...parent, children: children.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: 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, }; }; 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$3 = ".rst__rowWrapper {\n padding-block: 10px;\n padding-inline-end: 10px;\n padding-inline-start: 0;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n}\n\n.rst__rtl.rst__rowWrapper {\n padding-inline-start: 10px;\n padding-inline-end: 0;\n}\n\n.rst__row {\n height: 100%;\n white-space: nowrap;\n display: flex;\n position: relative;\n}\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::before,\n.rst__rowCancelPad::before {\n background-color: var(--rst-bg-landing);\n border: 3px dashed white;\n content: '';\n position: absolute;\n inset: 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: var(--rst-bg-cancel);\n}\n\n/**\n * Nodes matching the search conditions are highlighted\n */\n.rst__rowSearchMatch {\n outline: solid 3px var(--rst-match-color);\n}\n\n/**\n * The node that matches the search conditions and is currently focused\n */\n.rst__rowSearchFocus {\n outline: solid 3px var(--rst-focus-color);\n}\n\n.rst__rowContents {\n position: relative;\n height: 100%;\n border: solid #bbb 1px;\n border-inline-start: none;\n box-shadow: 0 2px 2px -2px rgba(0, 0, 0, 0.2);\n padding-inline: 10px 5px;\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 gap: 10px;\n}\n\n.rst__rowContentsDragDisabled {\n border-left: solid #bbb 1px;\n}\n\n.rst__rowLabel {\n flex: 0 1 auto;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.rst__rowToolbar {\n flex: 0 1 auto;\n display: flex;\n gap: 5px;\n}\n\n.rst__moveHandle,\n.rst__loadingHandle {\n height: 100%;\n width: var(--rst-handle-width);\n background-color: var(--rst-icon-color);\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MiIgaGVpZ2h0PSI0MiI+PGcgc3Ryb2tlPSIjRkZGIiBzdHJva2Utd2lkdGg9IjIuOSIgPjxwYXRoIGQ9Ik0xNCAxNS43aDE0LjQiLz48cGF0aCBkPSJNMTQgMjEuNGgxNC40Ii8+PHBhdGggZD0iTTE0IDI3LjFoMTQuNCIvPjwvZz4KPC9zdmc+');\n background-repeat: no-repeat;\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-color: #d9d9d9;\n background-image: none;\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 inset-inline-start: 0;\n top: 0;\n}\n\n@keyframes pointFade {\n 0%, 19.999%, 100% { opacity: 0; }\n 20% { opacity: 1; }\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\n.rst__loadingCirclePoint:nth-of-type(1) {\n transform: rotate(0deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(7) {\n transform: rotate(180deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(1)::before,\n.rst__loadingCirclePoint:nth-of-type(7)::before {\n animation-delay: -800ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(2) {\n transform: rotate(30deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(8) {\n transform: rotate(210deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(2)::before,\n.rst__loadingCirclePoint:nth-of-type(8)::before {\n animation-delay: -666ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(3) {\n transform: rotate(60deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(9) {\n transform: rotate(240deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(3)::before,\n.rst__loadingCirclePoint:nth-of-type(9)::before {\n animation-delay: -533ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(4) {\n transform: rotate(90deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(10) {\n transform: rotate(270deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(4)::before,\n.rst__loadingCirclePoint:nth-of-type(10)::before {\n animation-delay: -400ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(5) {\n transform: rotate(120deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(11) {\n transform: rotate(300deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(5)::before,\n.rst__loadingCirclePoint:nth-of-type(11)::before {\n animation-delay: -266ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(6) {\n transform: rotate(150deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(12) {\n transform: rotate(330deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(6)::before,\n.rst__loadingCirclePoint:nth-of-type(12)::before {\n animation-delay: -133ms;\n}\n\n.rst__loadingCirclePoint:nth-of-type(7) {\n transform: rotate(180deg);\n}\n\n.rst__loadingCirclePoint:nth-of-type(13) {\n transform: rotate(360deg);\n}\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: 0.85em;\n display: block;\n}\n\n.rst__rowSubtitle {\n font-size: 0.7em;\n opacity: 0.8;\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 translate: -50% -50%;\n cursor: pointer;\n background-color: var(--rst-button-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--rst-button-bg);\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.rst__collapseButton {\n /* SVG for Minus */\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48L2c+Cjwvc3ZnPg==');\n}\n\n.rst__expandButton {\n /* SVG for Plus */\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48cGF0aCBkPSJNOSA0LjV2OSIvPjwvZz4KPC9zdmc+');\n}\n\n\n.rst__collapseButton:focus,\n.rst__expandButton:focus {\n outline: none;\n box-shadow: 0 0 0 1px #000, 0 0 1px 3px var(--rst-primary-color);\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\n.rst__lineChildren::after {\n content: '';\n position: absolute;\n background-color: var(--rst-line-color);\n width: 1px;\n inset-inline-start: 50%;\n bottom: 0;\n height: 10px;\n}"; styleInject(css_248z$3); const NodeRendererDefault = ({ isSearchMatch = false, isSearchFocus = false, canDrag = false, toggleChildrenVisibility = undefined, buttons = [], className = '', style = {}, parentNode = undefined, draggedNode = undefined, canDrop = false, title = undefined, subtitle = undefined, rowDirection = 'ltr', scaffoldBlockPxWidth, connectDragPreview, connectDragSource, isDragging, node, path, treeIndex, didDrop, treeId: _treeId, isOver: _isOver, ...otherProps }) => { 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 ? (jsx("div", { className: "rst__loadingHandle", children: jsx("div", { className: "rst__loadingCircle", children: Array.from({ length: 12 }).map((_, index) => (jsx("div", { className: classnames('rst__loadingCirclePoint', rowDirectionClass ?? '') }, index))) }) })) : (jsx("div", { ref: connectDragSource, className: "rst__moveHandle" })); } 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 (jsxs("div", { style: { height: '100%' }, ...otherProps, children: [toggleChildrenVisibility && node.children && (node.children.length > 0 || typeof node.children === 'function') && (jsxs("div", { children: [jsx("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 && (jsx("div", { style: { width: scaffoldBlockPxWidth }, className: classnames('rst__lineChildren', rowDirectionClass ?? '') }))] })), jsx("div", { className: classnames('rst__rowWrapper', rowDirectionClass ?? ''), children: jsxs("div", { ref: connectDragPreview, 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, }, children: [handle, jsxs("div", { className: classnames('rst__rowContents', canDrag ? '' : 'rst__rowContentsDragDisabled', rowDirectionClass ?? ''), children: [jsxs("div", { className: classnames('rst__rowLabel', rowDirectionClass ?? ''), children: [jsx("span", { className: classnames('rst__rowTitle', node.subtitle ? 'rst__rowTitleWithSubtitle' : ''), children: typeof nodeTitle === 'function' ? nodeTitle({ node, path, treeIndex, }) : nodeTitle }), nodeSubtitle && (jsx("span", { className: "rst__rowSubtitle", children: typeof nodeSubtitle === 'function' ? nodeSubtitle({ node, path, treeIndex, }) : nodeSubtitle }))] }), jsx("div", { className: "rst__rowToolbar", children: buttons?.map((btn, index) => (jsx("div", { className: "rst__toolbarButton", children: btn }, index))) })] })] }) })] })); }; var css_248z$2 = ".rst__placeholder {\n position: relative;\n height: 68px;\n max-width: 300px;\n padding: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rst__placeholder,\n.rst__placeholder>* {\n box-sizing: border-box;\n}\n\n.rst__placeholder::before {\n border: 3px dashed #d9d9d9;\n content: '';\n position: absolute;\n inset: 5px;\n z-index: -1;\n border-radius: 4px;\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\n.rst__placeholderLandingPad *,\n.rst__placeholderCancelPad * {\n opacity: 0 !important;\n}\n\n.rst__placeholderLandingPad::before,\n.rst__placeholderCancelPad::before {\n background-color: var(--rst-bg-landing);\n border-color: white;\n border-style: solid;\n}\n\n/**\n * Alternate appearance of the landing pad when the dragged location is invalid\n */\n.rst__placeholderCancelPad::before {\n background-color: var(--rst-bg-cancel);\n}"; styleInject(css_248z$2); const PlaceholderRendererDefault = ({ isOver = false, canDrop = false, draggedNode: _draggedNode, }) => { return (jsx("div", { className: classnames('rst__placeholder', canDrop ? 'rst__placeholderLandingPad' : '', canDrop && !isOver ? 'rst__placeholderCancelPad' : '') })); }; var css_248z$1 = ".rst__node {\n min-width: 100%;\n white-space: nowrap;\n position: relative;\n text-align: start;\n height: var(--rst-row-height);\n box-sizing: border-box;\n display: flex;\n align-items: stretch;\n}\n\n.rst__nodeContent {\n position: absolute;\n inset-block: 0;\n}\n\n/* ==========================================================================\n Scaffold Lines\n ========================================================================== */\n.rst__lineBlock,\n.rst__absoluteLineBlock {\n height: 100%;\n position: relative;\n display: inline-block;\n flex-shrink: 0;\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: var(--rst-line-color);\n}\n\n/**\n * +-----+\n * | |\n * | +--+\n * | |\n * +-----+\n */\n.rst__lineHalfHorizontalRight::before {\n height: 1px;\n top: 50%;\n inset-inline-end: 0;\n width: 50%;\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 inset-inline-start: 50%;\n height: 100%;\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\n.rst__highlightLineVertical::before {\n position: absolute;\n content: '';\n background-color: var(--rst-line-highlight);\n width: 8px;\n inset-inline-start: 50%;\n translate: -50% 0;\n top: 0;\n height: 100%;\n}\n\n@keyframes arrow-pulse {\n 0% {\n translate: -50% 0;\n opacity: 0;\n }\n\n 30% {\n translate: -50% 300%;\n opacity: 1;\n }\n\n 70% {\n translate: -50% 700%;\n opacity: 1;\n }\n\n 100% {\n translate: -50% 1000%;\n opacity: 0;\n }\n}\n\n.rst__highlightLineVertical::after {\n content: '';\n position: absolute;\n height: 0;\n inset-inline-start: 50%;\n top: 0;\n border-inline: 4px solid transparent;\n border-top: 4px solid var(--rst-line-highlight-arrow);\n animation: arrow-pulse 1s infinite linear both;\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 var(--rst-line-highlight);\n border-inline-start: solid 8px var(--rst-line-highlight);\n box-sizing: border-box;\n height: calc(50% + 4px);\n top: 50%;\n margin-top: -4px;\n inset-inline-end: 0;\n width: calc(50% + 4px);\n}\n\n/**\n * +--+--+\n * | | |\n * | | |\n * | +->|\n * +-----+\n */\n.rst__highlightBottomLeftCorner {\n z-index: 3;\n}\n\n.rst__highlightBottomLeftCorner::before {\n content: '';\n position: absolute;\n border-bottom: solid 8px var(--rst-line-highlight);\n border-inline-start: solid 8px var(--rst-line-highlight);\n box-sizing: border-box;\n height: calc(100% + 4px);\n top: 0;\n inset-inline-end: 12px;\n width: calc(50% - 8px);\n}\n\n.rst__highlightBottomLeftCorner::after {\n content: '';\n position: absolute;\n height: 0;\n inset-inline-end: 0;\n top: 100%;\n margin-top: -12px;\n border-top: 12px solid transparent;\n border-bottom: 12px solid transparent;\n border-inline-start: 12px solid var(--rst-line-highlight);\n}"; styleInject(css_248z$1); const TreeNodeComponent = ({ children, listIndex, swapFrom = undefined, swapLength = undefined, swapDepth = undefined, scaffoldBlockPxWidth, lowerSiblingCounts, connectDropTarget, isOver, draggedNode = undefined, canDrop = false, treeIndex, rowHeight, rowDirection = 'ltr', // Extract props not used in DOM or needed by children directly treeId: _treeId, getPrevRow: _getPrevRow, node, path, ...otherProps }) => { 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 // +--+--+ // | | | // | | | // | | | // +--+--+ lineClass = 'rst__lineFullVertical'; } } else if (listIndex === 0) { // Top-left corner of the tree, but has no siblings // +-----+ // | | // | +--+ // | | // +-----+ lineClass = 'rst__lineHalfHorizontalRight'; } else if (i === scaffoldBlockCount - 1) { // The last or only node in this level of the tree // +--+--+ // | | | // | +--+ // | | // +-----+ lineClass = 'rst__lineHalfVerticalTop rst__lineHalfHorizontalRight'; } scaffold.push(jsx("div", { style: { width: scaffoldBlockPxWidth }, className: classnames('rst__lineBlock', lineClass, rowDirectionClass ?? '') }, `pre_${1 + i}`)); if (treeIndex !== listIndex && i === swapDepth) { // This row has been shifted, and is at the depth of // the line pointing to the new destination let highlightLineClass = ''; if (listIndex === swapFrom + swapLength - 1) { // This block is on the bottom (target) line // This block points at the target block (where the row will go when released) highlightLineClass = 'rst__highlightBottomLeftCorner'; } else if (treeIndex === swapFrom) { // This block is on the top (source) line highlightLineClass = 'rst__highlightTopLeftCorner'; } else { // This block is between the bottom and top highlightLineClass = 'rst__highlightLineVertical'; } const st