/** * 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'; 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([]); const [redoStack, setRedoStack] = useState([]); const canUndo = undoStack.length > 0; const canRedo = redoStack.length > 0; const store = useCallback(() => { // Deep clone the state to prevent mutations const stateSnapshot = JSON.parse(JSON.stringify(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 = JSON.parse(JSON.stringify(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 = JSON.parse(JSON.stringify(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, }; }