@nosferatu500/react-sortable-tree
Version:
Drag-and-drop sortable component for nested data and hierarchies
928 lines (914 loc) • 89.6 kB
JavaScript
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