/** * useVisualEditor Hook * * Central state management hook that coordinates between Canvas and StylePanel. * Handles: * - Selected element state * - Style modification logic * - Undo/redo functionality (basic implementation) * - Element reference management */ import { useState, useCallback, useRef } from 'react'; import { SelectedElement } from '../types/editor'; import { updateElementRect } from '../utils/selector'; interface StyleChange { element: HTMLElement; property: string; oldValue: string; newValue: string; } interface UseVisualEditorReturn { // State selectedElement: SelectedElement | null; showSpacing: boolean; showDimensions: boolean; // Actions selectElement: (element: SelectedElement | null) => void; deselectElement: () => void; applyStyle: (property: string, value: string) => void; toggleSpacing: () => void; toggleDimensions: () => void; // History canUndo: boolean; canRedo: boolean; undo: () => void; redo: () => void; } export function useVisualEditor(): UseVisualEditorReturn { // Core state const [selectedElement, setSelectedElement] = useState(null); const [showSpacing, setShowSpacing] = useState(false); const [showDimensions, setShowDimensions] = useState(true); // History for undo/redo const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // Ref to track the iframe window for updating computed styles const iframeWindowRef = useRef(null); /** * Select an element and store reference */ const selectElement = useCallback((element: SelectedElement | null) => { if (element) { // Store reference to iframe window for later use const ownerWindow = element.node.ownerDocument?.defaultView; if (ownerWindow) { iframeWindowRef.current = ownerWindow; } } setSelectedElement(element); }, []); /** * Deselect current element */ const deselectElement = useCallback(() => { setSelectedElement(null); }, []); /** * Apply a style change to the selected element */ const applyStyle = useCallback((property: string, value: string) => { if (!selectedElement) return; const element = selectedElement.node; // Store old value for undo const oldValue = element.style.getPropertyValue(property); // Apply the new style using setProperty for proper CSS handling // Convert camelCase to kebab-case for setProperty const kebabProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase(); if (value) { element.style.setProperty(kebabProperty, value); } else { element.style.removeProperty(kebabProperty); } // Add to history const change: StyleChange = { element, property: kebabProperty, oldValue, newValue: value }; setHistory(prev => { // Remove any redo history when making a new change const newHistory = prev.slice(0, historyIndex + 1); return [...newHistory, change]; }); setHistoryIndex(prev => prev + 1); // Update the selected element state to reflect new computed styles if (iframeWindowRef.current) { const updated = updateElementRect(selectedElement, iframeWindowRef.current); setSelectedElement(updated); } }, [selectedElement, historyIndex]); /** * Toggle spacing visualization */ const toggleSpacing = useCallback(() => { setShowSpacing(prev => !prev); }, []); /** * Toggle dimensions display */ const toggleDimensions = useCallback(() => { setShowDimensions(prev => !prev); }, []); /** * Undo last style change */ const undo = useCallback(() => { if (historyIndex < 0) return; const change = history[historyIndex]; if (change) { if (change.oldValue) { change.element.style.setProperty(change.property, change.oldValue); } else { change.element.style.removeProperty(change.property); } setHistoryIndex(prev => prev - 1); // Update selected element if it matches if (selectedElement && selectedElement.node === change.element && iframeWindowRef.current) { const updated = updateElementRect(selectedElement, iframeWindowRef.current); setSelectedElement(updated); } } }, [history, historyIndex, selectedElement]); /** * Redo last undone change */ const redo = useCallback(() => { if (historyIndex >= history.length - 1) return; const nextIndex = historyIndex + 1; const change = history[nextIndex]; if (change) { if (change.newValue) { change.element.style.setProperty(change.property, change.newValue); } else { change.element.style.removeProperty(change.property); } setHistoryIndex(nextIndex); // Update selected element if it matches if (selectedElement && selectedElement.node === change.element && iframeWindowRef.current) { const updated = updateElementRect(selectedElement, iframeWindowRef.current); setSelectedElement(updated); } } }, [history, historyIndex, selectedElement]); return { selectedElement, showSpacing, showDimensions, selectElement, deselectElement, applyStyle, toggleSpacing, toggleDimensions, canUndo: historyIndex >= 0, canRedo: historyIndex < history.length - 1, undo, redo }; } export default useVisualEditor;