import { useDraggable } from '@neodrag/react'; import { useReactFlow, type XYPosition } from '@xyflow/react'; import { type ReactNode, useCallback, useRef, useState } from 'react'; import useFlowStore from '../VisProgStores'; import styles from '../../VisProg.module.css'; import { NodeDefaults, type NodeTypes } from '../NodeRegistry' /** * Props for a draggable node within the drag-and-drop toolbar. * * @property className - Optional custom CSS classes for styling. * @property children - The visual content or label rendered inside the draggable node. * @property nodeType - The type of node represented (key from `NodeTypes`). * @property onDrop - Function called when the node is dropped on the flow pane. */ interface DraggableNodeProps { className?: string; children: ReactNode; nodeType: keyof typeof NodeTypes; onDrop: (nodeType: keyof typeof NodeTypes, position: XYPosition) => void; } /** * A draggable node element used in the drag-and-drop toolbar. * * Integrates with the NeoDrag library to handle drag events. * On drop, it calls the provided `onDrop` function with the node type and drop position. * * @param props - The draggable node configuration. * @returns A React element representing a draggable node. */ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) { const draggableRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); // The NeoDrag hook enables smooth drag functionality for this element. // @ts-expect-error: NeoDrag typing incompatibility — 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 (
{children}
); } /** * Adds a new node to the flow graph. * * Handles: * - Automatic node ID generation based on existing nodes of the same type. * - Loading of default data from the `NodeDefaults` registry. * - Integration with the flow store to update global node state. * * @param nodeType - The type of node to create (from `NodeTypes`). * @param position - The XY position in the flow canvas where the node will appear. */ function addNodeToFlow(nodeType: keyof typeof NodeTypes, position: XYPosition) { const { addNode } = useFlowStore.getState(); // Load any predefined data for this node type. const defaultData = NodeDefaults[nodeType] ?? {} const id = crypto.randomUUID(); // Create new node const newNode = { id: id, type: nodeType, position, data: JSON.parse(JSON.stringify(defaultData)) } addNode(newNode); } /** * The drag-and-drop toolbar component for the visual programming interface. * * Displays draggable node templates based on entries in `NodeDefaults`. * Each droppable node can be dragged into the flow pane to instantiate it. * * Automatically filters nodes whose `droppable` flag is set to `true`. * * @returns A React element representing the drag-and-drop toolbar. */ export function DndToolbar() { const { screenToFlowPosition } = useReactFlow(); /** * Handles dropping a node onto the flow pane. * Translates screen coordinates into flow coordinates using React Flow utilities. */ const handleNodeDrop = useCallback( (nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => { const flow = document.querySelector('.react-flow'); const flowRect = flow?.getBoundingClientRect(); // Only add the node if it is inside the flow canvas area. 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); addNodeToFlow(nodeType, position); } }, [screenToFlowPosition], ); // Map over the default nodes to get all nodes that can be dropped from the toolbar. const droppableNodes = Object.entries(NodeDefaults) .filter(([, data]) => data.droppable) .map(([type, data]) => ({ type: type as DraggableNodeProps['nodeType'], data })); return (
You can drag these nodes to the pane to create new nodes.
{/* Maps over all the nodes that are droppable, and puts them in the panel */} {droppableNodes.map(({type, data}) => ( {data.label} ))}
); }