108 lines
No EOL
4.2 KiB
TypeScript
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>;
|
|
} |