import React, { useState, useEffect, useCallback, useRef } from 'react'; import ToolLayout from '../components/ToolLayout'; import PreviewFrame from './components/PreviewFrame.fresh'; import Toolbar from './components/Toolbar'; import CodeInputs from './components/CodeInputs'; import InspectorSidebar from './components/InspectorSidebar'; import ElementEditor from './components/ElementEditor'; import '../styles/device-frames.css'; const HtmlPreviewTool = () => { const [htmlInput, setHtmlInput] = useState(''); const [cssInput, setCssInput] = useState(''); const [jsInput, setJsInput] = useState(''); const [selectedDevice, setSelectedDevice] = useState('mobile'); const [inspectMode, setInspectMode] = useState(false); const [inspectedElementInfo, setInspectedElementInfo] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); const [showSidebar, setShowSidebar] = useState(true); const [forceRender, setForceRender] = useState(0); // Separate inspector state to prevent iframe updates during inspector operations const [inspectorHtmlState, setInspectorHtmlState] = useState(''); const [isInspectorActive, setIsInspectorActive] = useState(false); // ENHANCED OPTION A: PreviewFrame API reference const previewFrameRef = useRef(null); // Debug: Monitor inspectedElementInfo changes and force re-render useEffect(() => { console.log('๐ STATE CHANGE: inspectedElementInfo updated to:', inspectedElementInfo); if (inspectedElementInfo) { console.log('๐ FORCING COMPONENT RE-RENDER for inspector sidebar'); // Force a re-render by updating a dummy state setForceRender(prev => prev + 1); } }, [inspectedElementInfo, forceRender]); const handleElementClick = useCallback((elementInfo) => { console.log('๐ ENHANCED ELEMENT CLICK:', elementInfo); if (elementInfo) { console.log('โ ENHANCED INSPECTOR: Activating with cascade-id:', elementInfo.cascadeId); setInspectedElementInfo(elementInfo); setIsInspectorActive(true); console.log('๐ฏ ENHANCED INSPECTOR: Sidebar activated, iframe DOM is source of truth'); // Debug: Force re-render check setTimeout(() => { console.log('๐ POST-SET DEBUG: inspectedElementInfo should now be:', elementInfo); setForceRender(prev => prev + 1); // Force re-render after state is set }, 10); } }, []); const cleanupInspectorState = useCallback(() => { console.log('๐งน ENHANCED OPTION A: Cleaning up inspector state without triggering iframe refresh'); console.log('๐จ DEBUG: cleanupInspectorState called - clearing inspectedElementInfo'); console.trace('๐ STACK TRACE: cleanupInspectorState called from:'); setInspectedElementInfo(null); setInspectMode(false); // ENHANCED OPTION A: Don't call setHtmlInput during cleanup // The iframe DOM cleanup will be handled by PreviewFrame directly // Only clean up React state, not HTML input console.log('โ ENHANCED CLEANUP: Inspector state cleared without iframe refresh'); }, []); // ESC key handler to deactivate inspect mode useEffect(() => { const handleKeyDown = (event) => { if (event.key === 'Escape' && inspectMode) { console.log('โจ๏ธ ESC key pressed - deactivating inspect mode'); cleanupInspectorState(); } }; // Add event listener document.addEventListener('keydown', handleKeyDown); // Cleanup return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [inspectMode, cleanupInspectorState]); useEffect(() => { // ENHANCED OPTION A: Skip cascade ID injection during inspector operations if (inspectedElementInfo) { console.log('๐ซ ENHANCED OPTION A: Skipping cascade ID injection during inspector operations'); return; } if (!htmlInput.trim()) return; const isFullDocument = htmlInput.trim().toLowerCase().includes(' { const idNum = parseInt(el.getAttribute('data-cascade-id').split('-')[1], 10); if (!isNaN(idNum) && idNum >= idCounter) { idCounter = idNum + 1; } }); // Add cascade IDs to elements that don't have them doc.querySelectorAll('*').forEach(el => { if (!el.hasAttribute('data-cascade-id') && el.tagName.toLowerCase() !== 'body' && el.tagName.toLowerCase() !== 'html' && el.tagName.toLowerCase() !== 'head' && el.tagName.toLowerCase() !== 'title' && el.tagName.toLowerCase() !== 'meta' && el.tagName.toLowerCase() !== 'link' && el.tagName.toLowerCase() !== 'style' && el.tagName.toLowerCase() !== 'script') { el.setAttribute('data-cascade-id', `cascade-${idCounter++}`); modified = true; } }); if (modified) { const newHtml = doc.documentElement.outerHTML; console.log('โ Full document processed, cascade IDs added'); console.log('๐จ CASCADE ID INJECTION: About to call setHtmlInput (POTENTIAL IFRAME REFRESH TRIGGER)'); setHtmlInput(newHtml); } } else { // It's a fragment, process normally const parser = new DOMParser(); const doc = parser.parseFromString(htmlInput, 'text/html'); let modified = false; let idCounter = 0; doc.body.querySelectorAll('[data-cascade-id]').forEach(el => { const idNum = parseInt(el.getAttribute('data-cascade-id').split('-')[1], 10); if (!isNaN(idNum) && idNum >= idCounter) { idCounter = idNum + 1; } }); doc.body.querySelectorAll('*').forEach(el => { if (!el.hasAttribute('data-cascade-id') && el.tagName.toLowerCase() !== 'body' && el.tagName.toLowerCase() !== 'html' && el.tagName.toLowerCase() !== 'head') { el.setAttribute('data-cascade-id', `cascade-${idCounter++}`); modified = true; } }); if (modified) { const newHtml = doc.body.innerHTML; console.log('โ Fragment processed, cascade IDs added'); console.log('๐จ CASCADE ID INJECTION: About to call setHtmlInput (POTENTIAL IFRAME REFRESH TRIGGER)'); setHtmlInput(newHtml); } } }, [htmlInput]); const createDuplicateInCodeBox = useCallback((elementInfo) => { const cascadeId = elementInfo.attributes['data-cascade-id']; if (!cascadeId) { console.error('โ Cannot create duplicate: Element is missing data-cascade-id.'); return false; } // Use stored iframe DOM that contains the cascade-id, fallback to inspector state const currentHtml = window.currentIframeDom || inspectorHtmlState || htmlInput; console.log('๐ฏ INSPECTOR: Creating duplicates using iframe DOM with cascade-id'); if (window.currentIframeDom) { console.log('โ Using current iframe DOM with cascade-id'); } else { console.log('โ ๏ธ Fallback to inspector state (cascade-id may be missing)'); } const processHtml = (currentHtml) => { const parser = new DOMParser(); const doc = parser.parseFromString(currentHtml, 'text/html'); const originalElement = doc.querySelector(`[data-cascade-id="${cascadeId}"]`); if (!originalElement) { console.error(`โ Could not find element with ${cascadeId} in HTML.`); return currentHtml; } const hiddenElement = originalElement.cloneNode(true); hiddenElement.setAttribute('data-original', 'true'); hiddenElement.style.display = 'none'; const visibleElement = originalElement.cloneNode(true); visibleElement.setAttribute('data-original', 'false'); originalElement.parentNode.insertBefore(hiddenElement, originalElement); originalElement.parentNode.insertBefore(visibleElement, originalElement); originalElement.remove(); console.log(`โ Successfully created duplicates for ${cascadeId}`); // Preserve the original HTML structure (full document vs fragment) const isFragment = !currentHtml.trim().toLowerCase().startsWith(' { // Refresh is handled by PreviewFrame component console.log('๐ Refreshing preview...'); }, []); const toggleFullscreen = useCallback((targetDevice = null) => { setIsFullscreen(prev => { const newFullscreen = !prev; // When exiting fullscreen (going to non-fullscreen), always switch to mobile if (!newFullscreen) { setSelectedDevice('mobile'); console.log('๐ฑ Exiting fullscreen: Switched to mobile view'); } return newFullscreen; }); if (targetDevice) { setSelectedDevice(targetDevice); } }, []); const toggleSidebar = useCallback(() => { setShowSidebar(prev => !prev); }, []); // ENHANCED OPTION A: Commit iframe DOM changes to HTML input using new API const saveInspectorChanges = useCallback(() => { console.log('๐พ ENHANCED COMMIT: Using PreviewFrame API to commit changes'); if (!previewFrameRef.current) { console.error('โ COMMIT FAILED: PreviewFrame ref not available'); return; } try { // Use Enhanced Option A API to get iframe DOM content const committedHtml = previewFrameRef.current.getIframeContent(); if (committedHtml) { // ENHANCED OPTION A: Update HTML input with committed changes // This is an EXPLICIT SAVE operation, so iframe refresh is expected and correct setHtmlInput(committedHtml); console.log('โ ENHANCED COMMIT: Changes committed successfully'); console.log('๐ COMMIT: HTML updated with iframe DOM content'); // Close inspector and reset state console.log('๐จ DEBUG: saveInspectorChanges called - clearing inspectedElementInfo'); setInspectedElementInfo(null); setInspectMode(false); setIsInspectorActive(false); console.log('๐ ENHANCED COMMIT: Inspector closed, iframe will refresh with new content'); } else { console.error('โ ENHANCED COMMIT: Failed to extract iframe DOM content'); } } catch (error) { console.error('โ ENHANCED COMMIT ERROR:', error); } }, [previewFrameRef]); // ENHANCED OPTION A: Close inspector and reset state const closeInspector = useCallback(() => { console.log('โ ENHANCED CLOSE: Closing inspector and resetting state'); // Reset all inspector state console.log('๐จ DEBUG: closeInspector called - clearing inspectedElementInfo'); setInspectedElementInfo(null); setInspectMode(false); setIsInspectorActive(false); setInspectorHtmlState(''); // Use PreviewFrame API to cancel changes if available if (previewFrameRef?.current?.cancelChanges) { previewFrameRef.current.cancelChanges(); } console.log('โ ENHANCED CLOSE: Inspector closed, iframe DOM reset'); }, [previewFrameRef]); const closeInspectorLegacy = useCallback(() => { // ENHANCED OPTION A: Close inspector and clear active flag (legacy) setIsInspectorActive(false); setInspectorHtmlState(''); window.isInspectorActive = false; console.log('โ INSPECTOR CLOSED: Iframe refreshes re-enabled'); cleanupInspectorState(); }, [cleanupInspectorState]); if (isFullscreen) { return (