Files
pepperplus-ui/src/pages/VisProgPage/visualProgrammingUI/EditorUndoRedo.ts
2026-01-06 15:12:00 +00:00

129 lines
3.6 KiB
TypeScript

import type {Edge, Node} from "@xyflow/react";
import type {StateCreator, StoreApi } from 'zustand/vanilla';
import type {FlowState} from "./VisProgTypes.tsx";
export type FlowSnapshot = {
nodes: Node[];
edges: Edge[];
}
/**
* A reduced version of the flowState type,
* This removes the functions that are provided by UndoRedo from the expected input type
*/
type BaseFlowState = Omit<FlowState, 'undo' | 'redo' | 'pushSnapshot' | 'beginBatchAction' | 'endBatchAction'>;
/**
* UndoRedo is implemented as a middleware for the FlowState store,
* this allows us to keep the undo redo logic separate from the flowState,
* and thus from the internal editor logic
*
* Allows users to undo and redo actions in the visual programming editor
*
* @param {(set: StoreApi<FlowState>["setState"], get: () => FlowState, api: StoreApi<FlowState>) => BaseFlowState} config
* @returns {StateCreator<FlowState>}
* @constructor
*/
export const UndoRedo = (
config: (
set: StoreApi<FlowState>['setState'],
get: () => FlowState,
api: StoreApi<FlowState>
) => BaseFlowState ) : StateCreator<FlowState> => (set, get, api) => {
let batchTimeout: number | null = null;
/**
* Captures the current state for
*
* @param {BaseFlowState} state - the current state of the editor
* @returns {FlowSnapshot} - returns a snapshot of the current editor state
*/
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
nodes: state.nodes,
edges: state.edges
}));
const initialState = config(set, get, api);
return {
...initialState,
/**
* Adds a snapshot of the current state to the undo history
*/
pushSnapshot: () => {
const state = get();
// we don't add new snapshots during an ongoing batch action
if (!state.isBatchAction) {
set({
past: [...state.past, getSnapshot(state)],
future: []
});
}
},
/**
* Undoes the last action from the editor,
* The state before undoing is added to the future for potential redoing
*/
undo: () => {
const state = get();
if (!state.past.length) return;
const snapshot = state.past.pop()!; // pop last snapshot
const currentSnapshot: FlowSnapshot = getSnapshot(state);
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
});
state.future.push(currentSnapshot); // push current to redo
},
/**
* redoes the last undone action,
* The state before redoing is added to the past for potential undoing
*/
redo: () => {
const state = get();
if (!state.future.length) return;
const snapshot = state.future.pop()!; // pop last redo
const currentSnapshot: FlowSnapshot = getSnapshot(state);
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
});
state.past.push(currentSnapshot); // push current to undo
},
/**
* Begins a batched action
*
* An example of a batched action is dragging a node in the editor,
* where we want the entire action of moving a node to a different position
* to be covered by one undoable snapshot
*/
beginBatchAction: () => {
get().pushSnapshot();
set({ isBatchAction: true });
if (batchTimeout) clearTimeout(batchTimeout);
},
/**
* Ends a batched action,
* a very short timeout is used to prevent new snapshots from being added
* until we are certain that the batch event is finished
*/
endBatchAction: () => {
batchTimeout = window.setTimeout(() => {
set({ isBatchAction: false });
}, 10);
}
}
}