dr-data
Fix iframe smooth transitions injection error
96d706b
"use client";
import { useUpdateEffect } from "react-use";
import { useMemo, useState, useRef, useEffect, forwardRef, useCallback } from "react";
import classNames from "classnames";
import { cn } from "@/lib/utils";
import { GridPattern } from "@/components/magic-ui/grid-pattern";
import { htmlTagToText } from "@/lib/html-tag-to-text";
export const Preview = forwardRef<
HTMLDivElement,
{
html: string;
isResizing: boolean;
isAiWorking: boolean;
device: "desktop" | "mobile";
currentTab: string;
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
isEditableModeEnabled?: boolean;
onClickElement?: (element: HTMLElement) => void;
}
>(({
html,
isResizing,
isAiWorking,
device,
currentTab,
iframeRef,
isEditableModeEnabled,
onClickElement,
}, ref) => {
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [displayHtml, setDisplayHtml] = useState(html);
const htmlUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const prevHtmlRef = useRef(html);
const internalIframeRef = useRef<HTMLIFrameElement>(null);
// Add iframe key for force refresh when needed
const [iframeKey, setIframeKey] = useState(0);
// Use internal ref if external ref not provided
const currentIframeRef = iframeRef || internalIframeRef;
// Force refresh iframe if it seems stuck
const forceRefresh = useCallback(() => {
console.log('🔄 Force refreshing iframe');
setIframeKey(prev => prev + 1);
}, []);
// Monitor for stuck updates and force refresh if needed
useEffect(() => {
if (html !== displayHtml && !isAiWorking) {
const timeout = setTimeout(() => {
if (html !== displayHtml) {
console.log('⚠️ Preview seems stuck, force refreshing');
setDisplayHtml(html);
forceRefresh();
}
}, 2000);
return () => clearTimeout(timeout);
}
}, [html, displayHtml, isAiWorking, forceRefresh]);
// Debug logging for initial state and prop changes
useEffect(() => {
console.log('🚀 Preview component mounted/updated with HTML:', {
htmlLength: html.length,
displayHtmlLength: displayHtml.length,
htmlPreview: html.substring(0, 200) + '...',
isAiWorking,
device,
currentTab
});
}, [html, displayHtml, isAiWorking, device, currentTab]);
// CRITICAL: Reliable HTML update logic with debugging
useEffect(() => {
console.log('🔄 Preview update triggered:', {
htmlLength: html.length,
isAiWorking,
displayHtmlLength: displayHtml.length,
htmlChanged: html !== displayHtml,
prevHtmlLength: prevHtmlRef.current.length,
htmlPreview: html.substring(0, 100) + '...',
displayPreview: displayHtml.substring(0, 100) + '...'
});
// ALWAYS update when HTML prop changes - this is critical
if (html !== prevHtmlRef.current) {
console.log('📝 HTML prop changed! Forcing update:', {
from: prevHtmlRef.current.length,
to: html.length,
isAiWorking,
willUseDelay: isAiWorking
});
// Clear any pending timeout to prevent conflicts
if (htmlUpdateTimeoutRef.current) {
clearTimeout(htmlUpdateTimeoutRef.current);
console.log('⏰ Cleared pending timeout');
}
// Update ref immediately
prevHtmlRef.current = html;
// For AI working (streaming), add minimal smoothness
if (isAiWorking && html.length > 0) {
console.log('🤖 AI working - adding minimal delay for smoothness');
setIsLoading(true);
htmlUpdateTimeoutRef.current = setTimeout(() => {
console.log('⚡ Applying delayed HTML update:', html.length);
setDisplayHtml(html);
setIsLoading(false);
}, 100); // Very short delay for minimal smoothness
} else {
// Immediate update for manual changes or when AI stops
console.log('⚡ Applying immediate HTML update:', html.length);
setDisplayHtml(html);
setIsLoading(false);
}
} else if (html !== displayHtml) {
// Edge case: displayHtml is out of sync
console.log('🔧 Fixing displayHtml sync issue');
setDisplayHtml(html);
}
console.log('✅ Preview update completed');
}, [html, isAiWorking, displayHtml]);
// Enhanced smooth transitions via CSS injection
useEffect(() => {
const iframe = currentIframeRef.current;
if (!iframe) return;
const injectSmoothTransitions = () => {
const doc = iframe.contentDocument;
if (!doc) {
console.warn('⚠️ Iframe document not available for smooth transitions');
return;
}
// Ensure document structure exists
if (!doc.head) {
console.warn('⚠️ Iframe document head not available for smooth transitions');
return;
}
// Check if document is ready
if (doc.readyState !== 'complete' && doc.readyState !== 'interactive') {
console.log('⏳ Document not ready, will retry smooth transitions injection');
setTimeout(injectSmoothTransitions, 50);
return;
}
const existingStyle = doc.getElementById('smooth-transitions');
if (existingStyle) {
console.log('🎨 Smooth transitions already injected, updating...');
existingStyle.remove();
}
try {
const style = doc.createElement('style');
style.id = 'smooth-transitions';
style.textContent = `
/* Enhanced smooth transitions for zero-flash updates */
* {
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.2s ease,
color 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease !important;
}
/* Prevent flash during content updates */
body {
transition: opacity 0.15s ease !important;
will-change: opacity;
}
/* Smooth content updates */
.content-updating {
animation: contentUpdate 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes contentUpdate {
0% {
opacity: 0.9;
transform: translateY(1px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* New element entrance */
.element-entering {
animation: elementEnter 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes elementEnter {
0% {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Subtle glow for changed content */
.content-changed {
animation: contentGlow 0.6s ease-in-out;
}
@keyframes contentGlow {
0%, 100% {
box-shadow: none;
background-color: transparent;
}
30% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
background-color: rgba(59, 130, 246, 0.05);
}
}
/* Optimize rendering performance */
* {
backface-visibility: hidden;
-webkit-font-smoothing: antialiased;
}
`;
doc.head.appendChild(style);
console.log('✨ Enhanced smooth transitions injected into iframe');
} catch (error) {
console.error('❌ Failed to inject smooth transitions:', error);
}
};
// Inject on iframe load and when content changes
const handleLoad = () => {
// Use multiple timeouts to ensure document is ready
setTimeout(injectSmoothTransitions, 10);
setTimeout(injectSmoothTransitions, 50);
setTimeout(injectSmoothTransitions, 100);
};
iframe.addEventListener('load', handleLoad);
// Also try to inject immediately if iframe is already loaded
if (iframe.contentDocument?.readyState === 'complete') {
injectSmoothTransitions();
} else {
// Try again after a short delay
setTimeout(() => {
if (iframe.contentDocument?.readyState === 'complete') {
injectSmoothTransitions();
}
}, 100);
}
return () => {
iframe.removeEventListener('load', handleLoad);
};
}, [currentIframeRef, iframeKey]);
// Enhanced content updating animations
useEffect(() => {
const iframe = currentIframeRef.current;
if (!iframe) return;
const addContentUpdateAnimation = () => {
const doc = iframe.contentDocument;
if (!doc || !doc.body) return;
const body = doc.body;
// Remove any existing animation classes
body.classList.remove('content-updating', 'content-changed');
// Add animation class
body.classList.add('content-updating');
console.log('🎬 Applied content update animation');
// Remove class after animation
const timeout = setTimeout(() => {
body.classList.remove('content-updating');
}, 300);
return () => {
clearTimeout(timeout);
body.classList.remove('content-updating', 'content-changed');
};
};
// Small delay to ensure iframe content is ready
const timeout = setTimeout(addContentUpdateAnimation, 50);
return () => {
clearTimeout(timeout);
};
}, [displayHtml, currentIframeRef]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (htmlUpdateTimeoutRef.current) {
clearTimeout(htmlUpdateTimeoutRef.current);
}
};
}, []);
// Event handlers for editable mode
const handleMouseOver = (event: MouseEvent) => {
if (currentIframeRef?.current) {
const iframeDocument = currentIframeRef.current.contentDocument;
if (iframeDocument) {
const targetElement = event.target as HTMLElement;
if (
hoveredElement !== targetElement &&
targetElement !== iframeDocument.body
) {
console.log("🎯 Edit mode: Element hovered", {
tagName: targetElement.tagName,
id: targetElement.id || 'no-id',
className: targetElement.className || 'no-class'
});
// Remove previous hover class
if (hoveredElement) {
hoveredElement.classList.remove("hovered-element");
}
setHoveredElement(targetElement);
targetElement.classList.add("hovered-element");
} else {
return setHoveredElement(null);
}
}
}
};
const handleMouseOut = () => {
setHoveredElement(null);
};
const handleClick = (event: MouseEvent) => {
console.log("🖱️ Edit mode: Click detected in iframe", {
target: event.target,
tagName: (event.target as HTMLElement)?.tagName,
isBody: event.target === currentIframeRef?.current?.contentDocument?.body,
hasOnClickElement: !!onClickElement
});
if (currentIframeRef?.current) {
const iframeDocument = currentIframeRef.current.contentDocument;
if (iframeDocument) {
const targetElement = event.target as HTMLElement;
if (targetElement !== iframeDocument.body) {
console.log("✅ Edit mode: Valid element clicked, calling onClickElement", {
tagName: targetElement.tagName,
id: targetElement.id || 'no-id',
className: targetElement.className || 'no-class',
textContent: targetElement.textContent?.substring(0, 50) + '...'
});
// Prevent default behavior to avoid navigation
event.preventDefault();
event.stopPropagation();
onClickElement?.(targetElement);
} else {
console.log("⚠️ Edit mode: Body clicked, ignoring");
}
} else {
console.error("❌ Edit mode: No iframe document available on click");
}
} else {
console.error("❌ Edit mode: No iframe ref available on click");
}
};
// Setup event listeners for editable mode
useUpdateEffect(() => {
const cleanupListeners = () => {
if (currentIframeRef?.current?.contentDocument) {
const iframeDocument = currentIframeRef.current.contentDocument;
iframeDocument.removeEventListener("mouseover", handleMouseOver);
iframeDocument.removeEventListener("mouseout", handleMouseOut);
iframeDocument.removeEventListener("click", handleClick);
console.log("🧹 Edit mode: Cleaned up iframe event listeners");
}
};
const setupListeners = () => {
try {
if (!currentIframeRef?.current) {
console.log("⚠️ Edit mode: No iframe ref available");
return;
}
const iframeDocument = currentIframeRef.current.contentDocument;
if (!iframeDocument) {
console.log("⚠️ Edit mode: No iframe content document available");
return;
}
// Clean up existing listeners first
cleanupListeners();
if (isEditableModeEnabled) {
console.log("🎯 Edit mode: Setting up iframe event listeners");
iframeDocument.addEventListener("mouseover", handleMouseOver);
iframeDocument.addEventListener("mouseout", handleMouseOut);
iframeDocument.addEventListener("click", handleClick);
console.log("✅ Edit mode: Event listeners added successfully");
} else {
console.log("🔇 Edit mode: Disabled, no listeners added");
}
} catch (error) {
console.error("❌ Edit mode: Error setting up listeners:", error);
}
};
// Add a small delay to ensure iframe is fully loaded
const timeoutId = setTimeout(setupListeners, 100);
// Clean up when component unmounts or dependencies change
return () => {
clearTimeout(timeoutId);
cleanupListeners();
};
}, [currentIframeRef, isEditableModeEnabled]);
const selectedElement = useMemo(() => {
if (!isEditableModeEnabled) return null;
if (!hoveredElement) return null;
return hoveredElement;
}, [hoveredElement, isEditableModeEnabled]);
return (
<div
ref={ref}
className={classNames(
"bg-white overflow-hidden relative flex-1 h-full",
{
"cursor-wait": isLoading && isAiWorking,
}
)}
onClick={(e) => {
e.stopPropagation();
}}
>
<GridPattern
width={20}
height={20}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className={cn(
"[mask-image:radial-gradient(300px_circle_at_center,white,transparent)] z-0 absolute inset-0 h-full w-full fill-neutral-100 stroke-neutral-100"
)}
/>
{/* Simplified loading overlay */}
{isLoading && isAiWorking && (
<div className="absolute inset-0 bg-black/5 backdrop-blur-[0.5px] transition-all duration-300 z-20 flex items-center justify-center">
<div className="bg-neutral-800/95 rounded-lg px-4 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
Updating preview...
</div>
</div>
</div>
)}
{/* Selected element indicator */}
{!isAiWorking && hoveredElement && selectedElement && (
<div className="absolute bottom-4 left-4 z-30">
<div className="bg-neutral-800/90 rounded-lg px-3 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
<span className="font-medium">
{htmlTagToText(selectedElement.tagName.toLowerCase())}
</span>
{selectedElement.id && (
<span className="ml-2 text-neutral-400">#{selectedElement.id}</span>
)}
</div>
</div>
)}
{/* Reliable iframe with force refresh capability */}
<iframe
key={iframeKey}
id="preview-iframe"
ref={currentIframeRef}
title="output"
className={classNames(
"w-full select-none h-full transition-all duration-200 ease-out",
{
"pointer-events-none": isResizing || isAiWorking,
"opacity-95 scale-[0.999]": isLoading && isAiWorking,
"opacity-100 scale-100": !isLoading || !isAiWorking,
"bg-black": true,
"lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
device === "mobile",
"lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
device === "desktop",
}
)}
srcDoc={displayHtml}
onLoad={() => {
console.log('🎯 Preview iframe loaded:', {
displayHtmlLength: displayHtml.length,
iframeKey,
hasContent: displayHtml.length > 0
});
setIsLoading(false);
}}
onError={(e) => {
console.error('❌ Iframe loading error:', e);
setIsLoading(false);
}}
/>
</div>
);
});
Preview.displayName = "Preview";