w-funnel/src/lib/admin/builder/useSimpleUndoRedo.ts
2025-09-27 23:05:02 +02:00

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,
};
}