- Removed HTML Preview Tool from navigation menu in Layout.js - Cleaned up unused Code import - Added HTML Preview related files to .gitignore - Project builds successfully without HTML Preview Tool
452 lines
17 KiB
JavaScript
452 lines
17 KiB
JavaScript
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 flex flex-col p-4">
|
|
<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={`flex flex-col 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;
|