1. Copy, paste, cut and delete functions added to the web page

This commit is contained in:
2026-05-16 22:30:36 +08:00
parent cd4f25a421
commit bc6e03cf9e
3 changed files with 927 additions and 0 deletions
+98
View File
@@ -637,6 +637,104 @@
const [gridSnap, setGridSnap] = useState(false);
// --- CLIPBOARD & ACTIONS ---
const [clipboard, setClipboard] = useState({ nodes: [] });
const handleCopy = useCallback(() => {
const currentNodes = reactFlowInstance.getNodes();
const selectedNodes = currentNodes.filter(n => n.selected);
if (selectedNodes.length > 0) {
// Deep clone the selected nodes to prevent reference issues
setClipboard({ nodes: JSON.parse(JSON.stringify(selectedNodes)) });
}
}, [reactFlowInstance]);
const handleCut = useCallback(() => {
const currentNodes = reactFlowInstance.getNodes();
const selectedNodes = currentNodes.filter(n => n.selected);
if (selectedNodes.length > 0) {
setClipboard({ nodes: JSON.parse(JSON.stringify(selectedNodes)) });
const selectedNodeIds = new Set(selectedNodes.map(n => n.id));
// Remove nodes and any edges connected to them
setNodes(nds => nds.filter(n => !selectedNodeIds.has(n.id)));
setEdges(eds => eds.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)));
}
}, [reactFlowInstance, setNodes, setEdges]);
const handlePaste = useCallback(() => {
if (clipboard.nodes.length > 0) {
const newNodes = clipboard.nodes.map(node => {
// Give the new node a unique ID and a slight positional offset
// so it doesn't overlap perfectly with the original
return {
...node,
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
position: {
x: node.position.x + 20,
y: node.position.y + 20
},
selected: true, // Automatically select the newly pasted node
data: {
...node.data,
// Ensure the pasted node gets a fresh display name (e.g., component_5)
componentDisplayName: generateComponentDisplayName()
}
};
});
// Deselect existing nodes and add the new ones
setNodes(nds => nds.map(n => ({...n, selected: false})).concat(newNodes));
// Update the clipboard with the new offset nodes so
// rapid pasting cascades them nicely
setClipboard({ nodes: newNodes });
}
}, [clipboard, setNodes, generateComponentDisplayName]);
const handleDelete = useCallback(() => {
const currentNodes = reactFlowInstance.getNodes();
const selectedNodes = currentNodes.filter(n => n.selected);
const selectedNodeIds = new Set(selectedNodes.map(n => n.id));
if (selectedNodeIds.size > 0) {
setNodes(nds => nds.filter(n => !selectedNodeIds.has(n.id)));
setEdges(eds => eds.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)));
}
}, [reactFlowInstance, setNodes, setEdges]);
// --- KEYBOARD SHORTCUTS ---
useEffect(() => {
const handleKeyDown = (e) => {
// Prevent actions if the user is typing in an input or textarea
if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
return;
}
const cmdOrCtrl = e.ctrlKey || e.metaKey; // Supports Windows/Linux (Ctrl) and Mac (Cmd)
if (cmdOrCtrl && e.key.toLowerCase() === 'c') {
e.preventDefault();
handleCopy();
} else if (cmdOrCtrl && e.key.toLowerCase() === 'x') {
e.preventDefault();
handleCut();
} else if (cmdOrCtrl && e.key.toLowerCase() === 'v') {
e.preventDefault();
handlePaste();
} else if (e.key === 'Delete' || e.key === 'Backspace') {
// ReactFlow handles this natively if the canvas is focused,
// but this ensures it fires even if focus is outside the canvas wrapper.
handleDelete();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleCopy, handleCut, handlePaste, handleDelete]);
const componentCounterRef = useRef(1);
const generateComponentDisplayName = useCallback(() => {