Files
pepperplus-ui/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx

106 lines
3.3 KiB
TypeScript

import { useDraggable } from '@neodrag/react';
import { useReactFlow, type XYPosition } from '@xyflow/react';
import { type ReactNode, useCallback, useRef, useState } from 'react';
import styles from '../../VisProg.module.css';
import { NodeDefaults, type NodeTypes } from '../NodeRegistry'
import addNode from '../utils/AddNode';
/**
* DraggableNodeProps dictates the type properties of a DraggableNode
*/
interface DraggableNodeProps {
className?: string;
children: ReactNode;
nodeType: keyof typeof NodeTypes;
onDrop: (nodeType: keyof typeof NodeTypes, position: XYPosition) => void;
}
/**
* Definition of a node inside the drag and drop toolbar.
* These nodes require an onDrop function that dictates
* how the node is created in the graph.
*/
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
const draggableRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });
// @ts-expect-error from the neodrag package — safe to ignore
useDraggable(draggableRef, {
position,
onDrag: ({ offsetX, offsetY }) => {
setPosition({ x: offsetX, y: offsetY });
},
onDragEnd: ({ event }) => {
setPosition({ x: 0, y: 0 });
onDrop(nodeType, { x: event.clientX, y: event.clientY });
},
});
return (
<div className={className}
ref={draggableRef}
id={`draggable-${nodeType}`}
data-testid={`draggable-${nodeType}`}
>
{children}
</div>
);
}
/**
* DndToolbar defines how the drag and drop toolbar component works
* and includes the default onDrop behavior.
*/
export function DndToolbar() {
const { screenToFlowPosition } = useReactFlow();
const handleNodeDrop = useCallback(
(nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => {
const flow = document.querySelector('.react-flow');
const flowRect = flow?.getBoundingClientRect();
const isInFlow =
flowRect &&
screenPosition.x >= flowRect.left &&
screenPosition.x <= flowRect.right &&
screenPosition.y >= flowRect.top &&
screenPosition.y <= flowRect.bottom;
if (isInFlow) {
const position = screenToFlowPosition(screenPosition);
addNode(nodeType, position);
}
},
[screenToFlowPosition],
);
// Map over our default settings to see which of them have their droppable data set to true
const droppableNodes = Object.entries(NodeDefaults)
.filter(([, data]) => data.droppable)
.map(([type, data]) => ({
type: type as DraggableNodeProps['nodeType'],
data
}));
return (
<div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`}>
<div className="description">
You can drag these nodes to the pane to create new nodes.
</div>
<div className={`flex-row gap-lg ${styles.dndNodeContainer}`}>
{/* Maps over all the nodes that are droppable, and puts them in the panel */}
{droppableNodes.map(({type, data}) => (
<DraggableNode
key={type}
className={styles[`draggable-node-${type}`]} // Our current style signature for nodes
nodeType={type}
onDrop={handleNodeDrop}
>
{data.label}
</DraggableNode>
))}
</div>
</div>
);
}