@stanfordbdhg/react-sortable-tree
Version:
Drag-and-drop sortable component for nested data and hierarchies
958 lines (944 loc) • 84.5 kB
JavaScript
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
// +--+--+
// | | |
// | | |
// | |