Fix HTML Preview Tool critical bugs: PreviewFrame ref and click handler
- Fixed missing ref={previewFrameRef} on first PreviewFrame component (line 336)
* This was causing 'PreviewFrame API not available' errors during save operations
* Both fullscreen and normal mode PreviewFrame instances now have proper ref connection
- Fixed click handler attachment bug in setupInspectModeStyles function
* Click handler was being skipped when styles were already injected
* Now always attaches click handler when inspect mode is activated
* Added proper cleanup to prevent duplicate event listeners
- Fixed variable scope issues in PreviewFrame.fresh.js
* styleElement and cursorStyleElement now properly scoped for cleanup function
* Added references to existing elements when styles already present
- Removed unused variables and fixed eslint warnings
* Removed unused indentSize variable in BeautifierTool.js
* Removed unused onSave and onDomUpdate props in PreviewFrame.fresh.js
* Fixed unnecessary escape character in script tag
These fixes restore the Enhanced Option A DOM manipulation architecture:
- Inspector sidebar should now appear when clicking elements in inspect mode
- Save functionality should work without 'PreviewFrame ref not available' errors
- Live editing of element properties should work through PreviewFrame API
- Iframe refresh prevention during inspector operations maintained
This commit is contained in:
451
src/pages/HtmlPreviewTool.js
Normal file
451
src/pages/HtmlPreviewTool.js
Normal file
@@ -0,0 +1,451 @@
|
||||
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('<html');
|
||||
console.log('🔧 HTML Processing Start:', { isFullDocument, inputLength: htmlInput.length });
|
||||
|
||||
// If it's already a full document, don't modify it unless we need to add cascade IDs
|
||||
if (isFullDocument) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlInput, 'text/html');
|
||||
let modified = false;
|
||||
let idCounter = 0;
|
||||
|
||||
// Count existing cascade IDs
|
||||
doc.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;
|
||||
}
|
||||
});
|
||||
|
||||
// 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('<html');
|
||||
return isFragment ? doc.body.innerHTML : doc.documentElement.outerHTML;
|
||||
};
|
||||
|
||||
// ENHANCED OPTION A: Process HTML and activate inspector (iframe DOM becomes source of truth)
|
||||
const processedHtml = processHtml(currentHtml);
|
||||
setInspectorHtmlState(processedHtml);
|
||||
setIsInspectorActive(true);
|
||||
window.isInspectorActive = true;
|
||||
console.log('🔍 INSPECTOR ACTIVATED: Iframe DOM is now source of truth, refreshes disabled');
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
// 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 (
|
||||
<div className="fixed inset-0 bg-gray-100 dark:bg-gray-900 z-50 flex flex-col">
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left sidebar - Code inputs */}
|
||||
{showSidebar && (
|
||||
<div className="w-96 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Code Editor</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-y-auto">
|
||||
<CodeInputs
|
||||
htmlInput={htmlInput}
|
||||
setHtmlInput={setHtmlInput}
|
||||
cssInput={cssInput}
|
||||
setCssInput={setCssInput}
|
||||
jsInput={jsInput}
|
||||
setJsInput={setJsInput}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Center - Preview */}
|
||||
<div className="flex-1 flex flex-col bg-gray-50 dark:bg-gray-900">
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<PreviewFrame
|
||||
ref={previewFrameRef}
|
||||
htmlInput={isInspectorActive ? inspectorHtmlState : htmlInput}
|
||||
cssInput={cssInput}
|
||||
jsInput={jsInput}
|
||||
selectedDevice={selectedDevice}
|
||||
isInspectModeActive={inspectMode}
|
||||
onElementClick={handleElementClick}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inspector Sidebar */}
|
||||
{inspectedElementInfo && (
|
||||
<InspectorSidebar
|
||||
inspectedElementInfo={inspectedElementInfo}
|
||||
htmlInput={htmlInput}
|
||||
setHtmlInput={setHtmlInput}
|
||||
onClose={closeInspector}
|
||||
onSave={saveInspectorChanges}
|
||||
previewFrameRef={previewFrameRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom toolbar */}
|
||||
<Toolbar
|
||||
selectedDevice={selectedDevice}
|
||||
setSelectedDevice={setSelectedDevice}
|
||||
isInspectModeActive={inspectMode}
|
||||
setInspectMode={setInspectMode}
|
||||
isFullscreen={isFullscreen}
|
||||
onRefresh={handleRefresh}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
onToggleSidebar={toggleSidebar}
|
||||
showSidebar={showSidebar}
|
||||
cleanupInspectorState={cleanupInspectorState}
|
||||
inspectedElementInfo={inspectedElementInfo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolLayout title="HTML Preview Tool">
|
||||
<div className={`flex h-full ${inspectedElementInfo ? 'gap-4' : 'gap-6'}`}>
|
||||
{/* Left column - Code inputs */}
|
||||
<div className={`space-y-6 transition-all duration-300 ${
|
||||
inspectedElementInfo ? 'flex-1' : 'w-1/2'
|
||||
}`}>
|
||||
<CodeInputs
|
||||
htmlInput={htmlInput}
|
||||
setHtmlInput={setHtmlInput}
|
||||
cssInput={cssInput}
|
||||
setCssInput={setCssInput}
|
||||
jsInput={jsInput}
|
||||
setJsInput={setJsInput}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Middle column - Preview */}
|
||||
<div className={`flex flex-col transition-all duration-300 ${
|
||||
inspectedElementInfo ? 'flex-1' : 'w-1/2'
|
||||
}`}>
|
||||
<div className="flex-1 bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
|
||||
<PreviewFrame
|
||||
ref={previewFrameRef}
|
||||
htmlInput={htmlInput}
|
||||
cssInput={cssInput}
|
||||
jsInput={jsInput}
|
||||
selectedDevice={selectedDevice}
|
||||
isInspectModeActive={inspectMode}
|
||||
onElementClick={handleElementClick}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ENHANCED OPTION A: Inspector Sidebar */}
|
||||
{inspectedElementInfo && (
|
||||
<div className="w-80 flex flex-col bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
|
||||
<InspectorSidebar
|
||||
inspectedElementInfo={inspectedElementInfo}
|
||||
htmlInput={htmlInput}
|
||||
setHtmlInput={setHtmlInput}
|
||||
onClose={closeInspector}
|
||||
onSave={saveInspectorChanges}
|
||||
previewFrameRef={previewFrameRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom toolbar */}
|
||||
<div className="mt-6">
|
||||
<Toolbar
|
||||
selectedDevice={selectedDevice}
|
||||
setSelectedDevice={setSelectedDevice}
|
||||
isInspectModeActive={inspectMode}
|
||||
setInspectMode={setInspectMode}
|
||||
isFullscreen={isFullscreen}
|
||||
onRefresh={handleRefresh}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
onToggleSidebar={toggleSidebar}
|
||||
showSidebar={showSidebar}
|
||||
cleanupInspectorState={cleanupInspectorState}
|
||||
inspectedElementInfo={inspectedElementInfo}
|
||||
/>
|
||||
</div>
|
||||
</ToolLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default HtmlPreviewTool;
|
||||
Reference in New Issue
Block a user