Phase 7.2 - Debounce undo/redo history: - Add 300ms debounce timer for rapid drag/transform changes - Commit history on dragEnd/transformEnd events only - Prevents history bloat during continuous interactions Phase 8.3 - Template-aware export: - Render template background layer first - Apply slot crop regions for image elements - Render template overlay layer last - Support nonPrintable flag for guides/watermarks Phase 9 - PWA icons: - Add pwa-192x192.svg and pwa-512x512.svg icons - Update vite.config.js manifest configuration Phase 10.3 - Performance optimizations: - Add React.memo to canvas components (ImageElement, TextElement, DesignCanvas) - Add React.memo to panel components (LayersPanel, PropertiesPanel) - Prevent unnecessary re-renders during canvas interactions Phase 10.6 - Template documentation: - Document template JSON schema in docs/template-schema.md - Include element properties, slot definitions, and examples - Describe background/overlay layer structure
186 lines
5.1 KiB
JavaScript
186 lines
5.1 KiB
JavaScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
|
const MAX_HISTORY = 50;
|
|
const DEBOUNCE_DELAY_MS = 300;
|
|
|
|
export function useDesignEditor() {
|
|
const [elements, setElements] = useState([]);
|
|
const [selectedId, setSelectedId] = useState(null);
|
|
|
|
// History for undo/redo
|
|
const historyRef = useRef([]);
|
|
const historyIndexRef = useRef(-1);
|
|
|
|
// Debounce timer for rapid changes (drag/transform)
|
|
const historyTimerRef = useRef(null);
|
|
const pendingChangesRef = useRef(null);
|
|
|
|
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++;
|
|
}
|
|
}, []);
|
|
|
|
// Flush pending changes to history
|
|
const flushPendingChanges = useCallback(() => {
|
|
if (pendingChangesRef.current) {
|
|
saveToHistory(pendingChangesRef.current);
|
|
pendingChangesRef.current = null;
|
|
}
|
|
if (historyTimerRef.current) {
|
|
clearTimeout(historyTimerRef.current);
|
|
historyTimerRef.current = null;
|
|
}
|
|
}, [saveToHistory]);
|
|
|
|
// Cleanup timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (historyTimerRef.current) {
|
|
clearTimeout(historyTimerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const canUndo = historyIndexRef.current > 0;
|
|
const canRedo = historyIndexRef.current < historyRef.current.length - 1;
|
|
|
|
const addElement = useCallback((element) => {
|
|
// Flush any pending debounced changes first
|
|
flushPendingChanges();
|
|
|
|
const newElement = {
|
|
...element,
|
|
id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
};
|
|
|
|
setElements((prev) => {
|
|
const newElements = [...prev, newElement];
|
|
saveToHistory(newElements);
|
|
return newElements;
|
|
});
|
|
|
|
setSelectedId(newElement.id);
|
|
return newElement.id;
|
|
}, [flushPendingChanges, saveToHistory]);
|
|
|
|
const updateElement = useCallback((id, attrs) => {
|
|
setElements((prev) => {
|
|
const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
|
|
|
|
// Debounce history commits for rapid changes (drag/transform)
|
|
// Store pending changes but don't commit yet
|
|
pendingChangesRef.current = newElements;
|
|
|
|
// Clear existing timer
|
|
if (historyTimerRef.current) {
|
|
clearTimeout(historyTimerRef.current);
|
|
}
|
|
|
|
// Set timer to commit changes after delay
|
|
historyTimerRef.current = setTimeout(() => {
|
|
flushPendingChanges();
|
|
}, DEBOUNCE_DELAY_MS);
|
|
|
|
return newElements;
|
|
});
|
|
}, [flushPendingChanges]);
|
|
|
|
const deleteElement = useCallback((id) => {
|
|
// Flush any pending debounced changes first
|
|
flushPendingChanges();
|
|
|
|
setElements((prev) => {
|
|
const newElements = prev.filter((el) => el.id !== id);
|
|
saveToHistory(newElements);
|
|
return newElements;
|
|
});
|
|
|
|
if (selectedId === id) {
|
|
setSelectedId(null);
|
|
}
|
|
}, [selectedId, flushPendingChanges, saveToHistory]);
|
|
|
|
const selectElement = useCallback((id) => {
|
|
setSelectedId(id);
|
|
}, []);
|
|
|
|
const deselectAll = useCallback(() => {
|
|
setSelectedId(null);
|
|
}, []);
|
|
|
|
const reorderElement = useCallback((id, newOrder) => {
|
|
// Flush any pending debounced changes first
|
|
flushPendingChanges();
|
|
|
|
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;
|
|
});
|
|
}, [flushPendingChanges, saveToHistory]);
|
|
|
|
// Commit history immediately (called on dragEnd/transformEnd)
|
|
const commitHistory = useCallback(() => {
|
|
flushPendingChanges();
|
|
}, [flushPendingChanges]);
|
|
|
|
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 {
|
|
elements,
|
|
selectedId,
|
|
addElement,
|
|
updateElement,
|
|
deleteElement,
|
|
selectElement,
|
|
deselectAll,
|
|
reorderElement,
|
|
commitHistory, // Call this on dragEnd/transformEnd to commit debounced changes
|
|
undo,
|
|
redo,
|
|
canUndo,
|
|
canRedo,
|
|
initializeHistory,
|
|
};
|
|
}
|