frontend/src/common/DraggableTabs.tsx

108 lines
No EOL
4.2 KiB
TypeScript

import { Tabs, TabsProps } from "@mui/material";
import React, { useEffect, useState, useRef } from "react";
import AnimatedTabs from "./AnimatedTabs";
interface DraggableTabsProps extends TabsProps {
children: React.ReactNode;
startNode?: React.ReactNode;
endNode?: React.ReactNode;
}
export default function DraggableTabs(props: DraggableTabsProps) {
const { startNode, endNode, children, ...tabsProps } = props;
const [selectedValue, setSelectedValue] = useState<any>(tabsProps.value);
const initialChildren = React.Children.toArray(children);
const [tabOrder, setTabOrder] = useState<number[]>([]);
const [hoveredIndex, setHoveredIndex] = useState<number>(-1); // Track hovered tab
const tabRefs = useRef<(HTMLDivElement | null)[]>([]); // Refs for each tab
useEffect(() => {
const newOrder = initialChildren.map((_, index) => index);
setTabOrder(newOrder);
tabRefs.current = new Array(newOrder.length).fill(null); // Initialize refs
}, [initialChildren.length]);
// Sync selectedValue with propValue if controlled
useEffect(() => {
if (tabsProps.value !== undefined) {
setSelectedValue(tabsProps.value); // Keep in sync with parent if controlled
}
}, [tabsProps.value]);
const flattenNodes = (nodes: React.ReactNode, prefix: string) =>
React.Children.toArray(nodes).flatMap((node, nodeIndex) => {
if (React.isValidElement(node) && (node.type as any) === React.Fragment) {
return React.Children.toArray(node.props.children).map((child, childIndex) =>
React.isValidElement(child)
? React.cloneElement(child, { key: `${prefix}-${nodeIndex}-${childIndex}` })
: child
);
}
return React.isValidElement(node)
? React.cloneElement(node, { key: `${prefix}-${nodeIndex}` })
: node;
});
const startTabs = flattenNodes(startNode, "start");
const endTabs = flattenNodes(endNode, "end");
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, index: number) => {
e.dataTransfer.setData("text/plain", index.toString());
e.dataTransfer.effectAllowed = "move";
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>, index: number) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setHoveredIndex(index); // Update hovered index
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const draggedIndex = Number(e.dataTransfer.getData("text/plain"));
const dropIndex = hoveredIndex; // Use the last hovered index
if (dropIndex === -1) {
console.log("Drop skipped: no valid drop target");
return;
}
if (draggedIndex !== dropIndex && draggedIndex >= 0 && dropIndex >= 0 && draggedIndex < tabOrder.length && dropIndex < tabOrder.length) {
const newOrder = [...tabOrder];
const [draggedItem] = newOrder.splice(draggedIndex, 1);
newOrder.splice(dropIndex, 0, draggedItem);
setTabOrder(newOrder);
const selectedIndex = selectedValue-startTabs.length
if (draggedIndex === selectedIndex)
setSelectedValue(dropIndex+startTabs.length)
else if (draggedIndex > selectedIndex && dropIndex <= selectedIndex)
setSelectedValue(selectedValue+1)
else if (draggedIndex < selectedIndex && dropIndex >= selectedIndex)
setSelectedValue(selectedValue-1)
} else {
console.log("Drop skipped: invalid indices or same position");
}
setHoveredIndex(-1); // Reset hovered index
};
const enhancedChildren = React.Children.map(initialChildren, (child, originalIndex) => {
if (React.isValidElement(child)) {
const currentIndex = tabOrder.indexOf(originalIndex);
return React.cloneElement(child, {
...child.props,
draggable: true,
onDragStart: (e: React.DragEvent<HTMLDivElement>) => handleDragStart(e, currentIndex),
onDragOver: (e: React.DragEvent<HTMLDivElement>) => handleDragOver(e, currentIndex),
onDrop: handleDrop,
});
}
return child;
}) || [];
const orderedChildren = tabOrder.map((index) => enhancedChildren[index]);
const allTabs = [...startTabs, ...orderedChildren, ...endTabs];
return <AnimatedTabs {...tabsProps} value={selectedValue}>{allTabs}</AnimatedTabs>;
}