106 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|