feat(network): add undo/redo shortcuts (Ctrl+Z/Y) and arrow key nudging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,9 @@ export function useCanvasShortcuts({
|
|||||||
setEdges,
|
setEdges,
|
||||||
setIsDirty,
|
setIsDirty,
|
||||||
canvasRef,
|
canvasRef,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
onNudge,
|
||||||
}: {
|
}: {
|
||||||
nodes: Node[]
|
nodes: Node[]
|
||||||
edges: Edge[]
|
edges: Edge[]
|
||||||
@@ -40,6 +43,9 @@ export function useCanvasShortcuts({
|
|||||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>
|
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>
|
||||||
setIsDirty: (dirty: boolean) => void
|
setIsDirty: (dirty: boolean) => void
|
||||||
canvasRef: React.RefObject<HTMLDivElement | null>
|
canvasRef: React.RefObject<HTMLDivElement | null>
|
||||||
|
onUndo: () => void
|
||||||
|
onRedo: () => void
|
||||||
|
onNudge: (dx: number, dy: number) => void
|
||||||
}) {
|
}) {
|
||||||
const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow()
|
const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow()
|
||||||
const clipboardRef = useRef<ClipboardData | null>(null)
|
const clipboardRef = useRef<ClipboardData | null>(null)
|
||||||
@@ -211,6 +217,31 @@ export function useCanvasShortcuts({
|
|||||||
|
|
||||||
const ctrl = e.ctrlKey || e.metaKey
|
const ctrl = e.ctrlKey || e.metaKey
|
||||||
|
|
||||||
|
// Undo: Ctrl+Z / Cmd+Z
|
||||||
|
if (e.key === 'z' && ctrl && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
onUndo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Redo: Ctrl+Y or Ctrl+Shift+Z
|
||||||
|
if ((e.key === 'y' && ctrl) || (e.key === 'z' && ctrl && e.shiftKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
onRedo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Arrow key nudging
|
||||||
|
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
const delta = e.shiftKey ? 10 : 1
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowUp': onNudge(0, -delta); break
|
||||||
|
case 'ArrowDown': onNudge(0, delta); break
|
||||||
|
case 'ArrowLeft': onNudge(-delta, 0); break
|
||||||
|
case 'ArrowRight': onNudge( delta, 0); break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (ctrl && e.key === 'c') {
|
if (ctrl && e.key === 'c') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
copyNodes()
|
copyNodes()
|
||||||
@@ -237,7 +268,7 @@ export function useCanvasShortcuts({
|
|||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack])
|
}, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copyNodes,
|
copyNodes,
|
||||||
|
|||||||
@@ -113,6 +113,17 @@ function DiagramEditorInner() {
|
|||||||
const canUndo = historyIndex.current > 0
|
const canUndo = historyIndex.current > 0
|
||||||
const canRedo = historyIndex.current < historyStack.current.length - 1
|
const canRedo = historyIndex.current < historyStack.current.length - 1
|
||||||
|
|
||||||
|
const onNudge = useCallback((dx: number, dy: number) => {
|
||||||
|
const selected = nodes.filter(n => n.selected)
|
||||||
|
if (selected.length === 0) return
|
||||||
|
pushHistory(nodes, edges)
|
||||||
|
setNodes(prev => prev.map(n =>
|
||||||
|
n.selected
|
||||||
|
? { ...n, position: { x: n.position.x + dx, y: n.position.y + dy } }
|
||||||
|
: n
|
||||||
|
))
|
||||||
|
}, [nodes, edges, pushHistory, setNodes])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
copyNodes,
|
copyNodes,
|
||||||
pasteNodes,
|
pasteNodes,
|
||||||
@@ -127,6 +138,9 @@ function DiagramEditorInner() {
|
|||||||
setEdges,
|
setEdges,
|
||||||
setIsDirty: (v: boolean) => setIsDirty(v),
|
setIsDirty: (v: boolean) => setIsDirty(v),
|
||||||
canvasRef,
|
canvasRef,
|
||||||
|
onUndo: undo,
|
||||||
|
onRedo: redo,
|
||||||
|
onNudge,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleNodesChange: typeof onNodesChange = useCallback((changes) => {
|
const handleNodesChange: typeof onNodesChange = useCallback((changes) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user