Undo and redo working

This commit is contained in:
Lucas Oliveira 2023-05-05 12:18:47 -03:00
commit 9cc4d84580
2 changed files with 154 additions and 18 deletions

View file

@ -0,0 +1,105 @@
import { useCallback, useEffect, useState } from 'react';
import { Edge, Node, useReactFlow } from 'reactflow';
type UseUndoRedoOptions = {
maxHistorySize: number;
enableShortcuts: boolean;
};
type UseUndoRedo = (options?: UseUndoRedoOptions) => {
undo: () => void;
redo: () => void;
takeSnapshot: () => void;
canUndo: boolean;
canRedo: boolean;
};
type HistoryItem = {
nodes: Node[];
edges: Edge[];
};
const defaultOptions: UseUndoRedoOptions = {
maxHistorySize: 100,
enableShortcuts: true,
};
// https://redux.js.org/usage/implementing-undo-history
export const useUndoRedo: UseUndoRedo = ({
maxHistorySize = defaultOptions.maxHistorySize,
enableShortcuts = defaultOptions.enableShortcuts,
} = defaultOptions) => {
// the past and future arrays store the states that we can jump to
const [past, setPast] = useState<HistoryItem[]>([]);
const [future, setFuture] = useState<HistoryItem[]>([]);
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
const takeSnapshot = useCallback(() => {
// push the current graph to the past state
setPast((past) => [
...past.slice(past.length - maxHistorySize + 1, past.length),
{ nodes: getNodes(), edges: getEdges() },
]);
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
setFuture([]);
}, [getNodes, getEdges, maxHistorySize]);
const undo = useCallback(() => {
// get the last state that we want to go back to
const pastState = past[past.length - 1];
if (pastState) {
// first we remove the state from the history
setPast((past) => past.slice(0, past.length - 1));
// we store the current graph for the redo operation
setFuture((future) => [...future, { nodes: getNodes(), edges: getEdges() }]);
// now we can set the graph to the past state
setNodes(pastState.nodes);
setEdges(pastState.edges);
}
}, [setNodes, setEdges, getNodes, getEdges, past]);
const redo = useCallback(() => {
const futureState = future[future.length - 1];
if (futureState) {
setFuture((future) => future.slice(0, future.length - 1));
setPast((past) => [...past, { nodes: getNodes(), edges: getEdges() }]);
setNodes(futureState.nodes);
setEdges(futureState.edges);
}
}, [setNodes, setEdges, getNodes, getEdges, future]);
useEffect(() => {
// this effect is used to attach the global event handlers
if (!enableShortcuts) {
return;
}
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
redo();
} else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) {
undo();
}
};
document.addEventListener('keydown', keyDownHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
};
}, [undo, redo, enableShortcuts]);
return {
undo,
redo,
takeSnapshot,
canUndo: !past.length,
canRedo: !future.length,
};
};
export default useUndoRedo;

View file

@ -12,6 +12,10 @@ import ReactFlow, {
Edge,
useKeyPress,
useOnSelectionChange,
NodeDragHandler,
OnEdgesDelete,
OnNodesDelete,
SelectionDragHandler,
} from "reactflow";
import { locationContext } from "../../contexts/locationContext";
import ExtraSidebar from "./components/extraSidebarComponent";
@ -24,6 +28,7 @@ import ConnectionLineComponent from "./components/ConnectionLineComponent";
import { FlowType, NodeType } from "../../types/flow";
import { APIClassType } from "../../types/api";
import { isValidConnection } from "../../utils";
import useUndoRedo from "./hooks/useUndoRedo";
const nodeTypes = {
genericNode: GenericNode,
@ -38,13 +43,25 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
useContext(typesContext);
const reactFlowWrapper = useRef(null);
const copied = useKeyPress(['Meta+c', 'Strg+c'])
const pasted = useKeyPress(['Meta+v', 'Strg+v'])
const undo = useKeyPress(['Meta+z', 'Strg+z'])
const redo = useKeyPress(['Meta+Shift+z', 'Strg+Shift+z'])
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
const copied = useKeyPress(['Meta+c', 'Strg+c']);
const pasted = useKeyPress(['Meta+v', 'Strg+v']);
const undoed = useKeyPress(['Meta+z', 'Strg+z']);
const redoed = useKeyPress(['Meta+Shift+z', 'Strg+Shift+z']);
useEffect(() => {
if(canUndo && undoed){
undo();
}
if(canRedo && redoed){
redo();
}
}, [undoed, redoed])
const [lastSelection, setLastSelection] = useState(null);
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
const [actualActionIndex] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
@ -56,15 +73,6 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
onChange: (flow) => {setLastSelection(flow);},
})
useEffect(() => {
if(undo) {
}
else if(redo) {
}
}, [redo, undo])
useEffect(() => {
if(copied === true && lastSelection){
setLastCopiedSelection(lastSelection);
@ -180,6 +188,7 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
const onConnect = useCallback(
(params: Connection) => {
takeSnapshot();
setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds)
);
@ -188,9 +197,26 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
return newX;
});
},
[setEdges, setNodes]
[setEdges, setNodes, takeSnapshot]
);
const onNodeDragStart: NodeDragHandler = useCallback(() => {
// 👇 make dragging a node undoable
takeSnapshot();
// 👉 you can place your event handlers here
}, [takeSnapshot]);
const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
// 👇 make dragging a selection undoable
takeSnapshot();
}, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
@ -199,6 +225,7 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
takeSnapshot();
// Get the current bounds of the ReactFlow wrapper element
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
@ -246,16 +273,17 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
}
},
// Specify dependencies for useCallback
[incrementNodeId, reactFlowInstance, setErrorData, setNodes]
[incrementNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
);
const onDelete = (mynodes) => {
const onDelete = useCallback((mynodes) => {
takeSnapshot();
setEdges(
edges.filter(
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
)
);
};
}, [takeSnapshot, edges, setEdges]);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
@ -298,6 +326,9 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}