148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
/**
|
|
* Simple Undo/Redo Hook for Builder State
|
|
* Based on Memento pattern - stores complete state snapshots
|
|
*/
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import type { BuilderState } from './context';
|
|
|
|
function cloneState<T>(value: T): T {
|
|
if (typeof structuredClone === 'function') {
|
|
return structuredClone(value);
|
|
}
|
|
|
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
}
|
|
|
|
interface UndoRedoHook {
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
undo: () => void;
|
|
redo: () => void;
|
|
store: () => void; // Save current state to undo stack
|
|
clear: () => void;
|
|
}
|
|
|
|
export function useSimpleUndoRedo(
|
|
currentState: BuilderState,
|
|
onStateChange: (state: BuilderState) => void,
|
|
maxHistorySize: number = 50
|
|
): UndoRedoHook {
|
|
const [undoStack, setUndoStack] = useState<BuilderState[]>([]);
|
|
const [redoStack, setRedoStack] = useState<BuilderState[]>([]);
|
|
|
|
const canUndo = undoStack.length > 0;
|
|
const canRedo = redoStack.length > 0;
|
|
|
|
const store = useCallback(() => {
|
|
// Deep clone the state to prevent mutations
|
|
const stateSnapshot = cloneState(currentState);
|
|
|
|
setUndoStack(prev => {
|
|
const newStack = [...prev, stateSnapshot];
|
|
|
|
// Limit history size
|
|
if (newStack.length > maxHistorySize) {
|
|
return newStack.slice(-maxHistorySize);
|
|
}
|
|
|
|
return newStack;
|
|
});
|
|
|
|
// Clear redo stack when new state is stored
|
|
setRedoStack([]);
|
|
}, [currentState, maxHistorySize]);
|
|
|
|
const undo = useCallback(() => {
|
|
if (!canUndo) return;
|
|
|
|
const lastState = undoStack[undoStack.length - 1];
|
|
|
|
// Move current state to redo stack
|
|
const currentSnapshot = cloneState(currentState);
|
|
setRedoStack(prev => [...prev, currentSnapshot]);
|
|
|
|
// Remove last state from undo stack
|
|
setUndoStack(prev => prev.slice(0, prev.length - 1));
|
|
|
|
// Apply the previous state
|
|
onStateChange(lastState);
|
|
}, [canUndo, undoStack, currentState, onStateChange]);
|
|
|
|
const redo = useCallback(() => {
|
|
if (!canRedo) return;
|
|
|
|
const lastRedoState = redoStack[redoStack.length - 1];
|
|
|
|
// Move current state to undo stack
|
|
const currentSnapshot = cloneState(currentState);
|
|
setUndoStack(prev => [...prev, currentSnapshot]);
|
|
|
|
// Remove last state from redo stack
|
|
setRedoStack(prev => prev.slice(0, prev.length - 1));
|
|
|
|
// Apply the redo state
|
|
onStateChange(lastRedoState);
|
|
}, [canRedo, redoStack, currentState, onStateChange]);
|
|
|
|
const clear = useCallback(() => {
|
|
setUndoStack([]);
|
|
setRedoStack([]);
|
|
}, []);
|
|
|
|
// Keyboard shortcuts
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Only handle shortcuts when not in input elements
|
|
const target = event.target as HTMLElement;
|
|
if (
|
|
target.tagName === 'INPUT' ||
|
|
target.tagName === 'TEXTAREA' ||
|
|
target.tagName === 'SELECT' ||
|
|
target.contentEditable === 'true'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
|
|
|
|
if (isCtrlOrCmd && event.key === 'z') {
|
|
event.preventDefault();
|
|
|
|
if (event.shiftKey) {
|
|
// Ctrl+Shift+Z or Cmd+Shift+Z for redo
|
|
if (canRedo) {
|
|
redo();
|
|
}
|
|
} else {
|
|
// Ctrl+Z or Cmd+Z for undo
|
|
if (canUndo) {
|
|
undo();
|
|
}
|
|
}
|
|
} else if (isCtrlOrCmd && event.key === 'y') {
|
|
// Ctrl+Y for redo (Windows standard)
|
|
event.preventDefault();
|
|
if (canRedo) {
|
|
redo();
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [canUndo, canRedo, undo, redo]);
|
|
|
|
return {
|
|
canUndo,
|
|
canRedo,
|
|
undo,
|
|
redo,
|
|
store,
|
|
clear,
|
|
};
|
|
}
|