Phase 7: Undo/Redo
- History tracking with 50-state limit in useDesignEditor hook - Undo: Ctrl/Cmd + Z keyboard shortcut - Redo: Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y - Undo/Redo buttons in canvas header - History saves state after add, update, delete, reorder operations - Disabled button states when history is exhausted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,70 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
|
||||
const MAX_HISTORY = 50;
|
||||
|
||||
export function useDesignEditor() {
|
||||
const [elements, setElements] = useState([]);
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
|
||||
// History for undo/redo
|
||||
const historyRef = useRef([]);
|
||||
const historyIndexRef = useRef(-1);
|
||||
|
||||
const saveToHistory = useCallback((newElements) => {
|
||||
// Remove any future history if we're in the middle of the stack
|
||||
if (historyIndexRef.current < historyRef.current.length - 1) {
|
||||
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
|
||||
}
|
||||
|
||||
// Add new state to history
|
||||
historyRef.current.push(JSON.stringify(newElements));
|
||||
|
||||
// Limit history size
|
||||
if (historyRef.current.length > MAX_HISTORY) {
|
||||
historyRef.current.shift();
|
||||
} else {
|
||||
historyIndexRef.current++;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const canUndo = historyIndexRef.current > 0;
|
||||
const canRedo = historyIndexRef.current < historyRef.current.length - 1;
|
||||
|
||||
const addElement = useCallback((element) => {
|
||||
const newElement = {
|
||||
...element,
|
||||
id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
};
|
||||
setElements((prev) => [...prev, newElement]);
|
||||
|
||||
setElements((prev) => {
|
||||
const newElements = [...prev, newElement];
|
||||
saveToHistory(newElements);
|
||||
return newElements;
|
||||
});
|
||||
|
||||
setSelectedId(newElement.id);
|
||||
return newElement.id;
|
||||
}, []);
|
||||
}, [saveToHistory]);
|
||||
|
||||
const updateElement = useCallback((id, attrs) => {
|
||||
setElements((prev) =>
|
||||
prev.map((el) => (el.id === id ? { ...el, ...attrs } : el))
|
||||
);
|
||||
}, []);
|
||||
setElements((prev) => {
|
||||
const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
|
||||
saveToHistory(newElements);
|
||||
return newElements;
|
||||
});
|
||||
}, [saveToHistory]);
|
||||
|
||||
const deleteElement = useCallback((id) => {
|
||||
setElements((prev) => prev.filter((el) => el.id !== id));
|
||||
setElements((prev) => {
|
||||
const newElements = prev.filter((el) => el.id !== id);
|
||||
saveToHistory(newElements);
|
||||
return newElements;
|
||||
});
|
||||
|
||||
if (selectedId === id) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}, [selectedId]);
|
||||
}, [selectedId, saveToHistory]);
|
||||
|
||||
const selectElement = useCallback((id) => {
|
||||
setSelectedId(id);
|
||||
@@ -39,11 +78,38 @@ export function useDesignEditor() {
|
||||
setElements((prev) => {
|
||||
const index = prev.findIndex((el) => el.id === id);
|
||||
if (index === -1 || index === newOrder) return prev;
|
||||
|
||||
const newElements = [...prev];
|
||||
const [removed] = newElements.splice(index, 1);
|
||||
newElements.splice(newOrder, 0, removed);
|
||||
|
||||
saveToHistory(newElements);
|
||||
return newElements;
|
||||
});
|
||||
}, [saveToHistory]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (historyIndexRef.current > 0) {
|
||||
historyIndexRef.current--;
|
||||
const prevState = JSON.parse(historyRef.current[historyIndexRef.current]);
|
||||
setElements(prevState);
|
||||
setSelectedId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (historyIndexRef.current < historyRef.current.length - 1) {
|
||||
historyIndexRef.current++;
|
||||
const nextState = JSON.parse(historyRef.current[historyIndexRef.current]);
|
||||
setElements(nextState);
|
||||
setSelectedId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initialize history with empty state
|
||||
const initializeHistory = useCallback(() => {
|
||||
historyRef.current = [JSON.stringify([])];
|
||||
historyIndexRef.current = 0;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
@@ -55,5 +121,10 @@ export function useDesignEditor() {
|
||||
selectElement,
|
||||
deselectAll,
|
||||
reorderElement,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
initializeHistory,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user