Files
dewedev/src/pages/HtmlPreviewTool.js
dwindown 76c0a0d014 Remove HTML Preview Tool from navigation and add to gitignore
- 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
2025-08-04 13:04:11 +07:00

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;