Files
pepperplus-ui/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx
JGerla a9effb7c23 style: added comments to code and changed name of Sidebar to DndToolbar
BREAKING: renamed Sidebar to DndToolbar.

ref: N25B-114
2025-10-12 13:48:42 +02:00

173 lines
4.6 KiB
TypeScript

import {useDraggable} from '@neodrag/react';
import {
useReactFlow,
type XYPosition
} from '@xyflow/react';
import {
type ReactNode,
useCallback,
useRef,
useState
} from 'react';
// Is used to make sure each subsequent node gets a unique id, so the ReactFlow implementation
// can distinguish between different nodes.
let id = 0;
const getId = () => `dndnode_${id++}`;
/**
* DraggableNodeProps dictates the type properties of a DraggableNode
*/
interface DraggableNodeProps {
className?: string;
children: ReactNode;
nodeType: string;
onDrop: (nodeType: string, position: XYPosition) => void;
}
/**
* Definition of a node inside the drag and drop toolbar,
* these nodes require an onDrop function to be supplied
* that dictates how the node is created in the graph.
*
* @param className
* @param children
* @param nodeType
* @param onDrop
* @constructor
*/
function DraggableNode({className, children, nodeType, onDrop}: DraggableNodeProps) {
const draggableRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({x: 0, y: 0});
// @ts-ignore
useDraggable(draggableRef, {
position: position,
onDrag: ({offsetX, offsetY}) => {
// Calculate position relative to the viewport
setPosition({
x: offsetX,
y: offsetY,
});
},
onDragEnd: ({event}) => {
setPosition({x: 0, y: 0});
onDrop(nodeType, {
x: event.clientX,
y: event.clientY,
});
},
});
return (
<div className={className === "default" ? "draggable-node" : "draggable-node" + "__" + className}
ref={draggableRef}>
{children}
</div>
);
}
/**
* the DndToolbar defines how the drag and drop toolbar component works
* and includes the default onDrop behavior through handleNodeDrop
* @constructor
*/
export function DndToolbar() {
const {setNodes, screenToFlowPosition} = useReactFlow();
/**
* handleNodeDrop implements the default onDrop behavior
*/
const handleNodeDrop = useCallback(
(nodeType: string, 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;
// Create a new node and add it to the flow
if (isInFlow) {
const position = screenToFlowPosition(screenPosition);
const newNode = () => {
switch (nodeType) {
case "phase":
return {
id: getId(),
type: nodeType,
position,
data: {label: `"new"`, number: (-1)},
};
case "start":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new start node`},
};
case "end":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new end node`},
};
case "norm":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new norm node`},
};
default: {
return {
id: getId(),
type: nodeType,
position,
data: {label: `new default node`},
};
}
}
}
setNodes((nds) => nds.concat(newNode()));
}
},
[setNodes, screenToFlowPosition],
);
return (
<div style={{
padding: '10px 10px',
outline: '2.5pt solid black',
borderRadius: '0pt 0pt 5pt 5pt',
borderColor: 'dimgrey',
backgroundColor: 'canvas',
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<div className="description">
You can drag these nodes to the pane to create new nodes.
</div>
<div className="DraggableNodeContainer" style={{
backgroundColor: 'canvas',
display: 'flex',
flexDirection: 'row',
gap: '1rem',
justifyContent: 'center'
}}>
<DraggableNode className="phase" nodeType="phase" onDrop={handleNodeDrop}>
phase Node
</DraggableNode>
<DraggableNode className="norm" nodeType="norm" onDrop={handleNodeDrop}>
norm Node
</DraggableNode>
</div>
</div>
);
}