import { useState, useContext, useEffect, useLayoutEffect, useRef, useCallback } from 'react' import PropTypes from 'prop-types' import { Flex, Button, Input, Select } from 'antd' import PlusIcon from '../../Icons/PlusIcon.jsx' import MinusIcon from '../../Icons/MinusIcon.jsx' import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' import PanIcon from '../../Icons/PanIcon.jsx' import PanFilledIcon from '../../Icons/PanFilledIcon.jsx' import ObjectProperty from '../common/ObjectProperty.jsx' import ScrollBox from './ScrollBox.jsx' import { ApiServerContext } from '../context/ApiServerContext.jsx' const noop = () => {} const TemplatePreview = ({ objectData, documentTemplate, loading, isEditing, onTestObjectOpen = noop, onPreviewMessage = noop, showTestObject = false, showPreviewSwitch = true }) => { const iframeRef = useRef(null) const scrollElementRef = useRef(null) const savedScrollRef = useRef({ top: 0, left: 0 }) const iframeSizeObserverRef = useRef(null) const previewRequestIdRef = useRef(0) const { fetchTemplatePreview, fetchTemplatePDF } = useContext(ApiServerContext) const [previewContentHTML, setPreviewContentHTML] = useState('') const [pdfBlob, setPDFBlob] = useState(null) const [reloadLoading, setReloadLoading] = useState(false) const [previewScale, setPreviewScale] = useState(1) const [previewType, setPreviewType] = useState('HTML') const [panMode, setPanMode] = useState(false) const [isPanning, setIsPanning] = useState(false) const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }) const [iframeSize, setIframeSize] = useState({ width: '100%', height: 'auto' }) const applyIframeContentSize = useCallback(() => { const iframe = iframeRef.current const doc = iframe?.contentDocument if (!iframe || !doc?.documentElement) { return } const documentEl = doc.querySelector('.previewDocument') || doc.querySelector('.previewWrapper') || doc.body const container = doc.querySelector('.previewContainer') const wrapper = doc.querySelector('.previewWrapper') const visual = documentEl.getBoundingClientRect() if (visual.width <= 0 && visual.height <= 0) { return } if (wrapper) { wrapper.style.width = `${visual.width}px` wrapper.style.height = `${visual.height}px` } let padX = 0 let padY = 0 if (container) { const containerStyle = doc.defaultView.getComputedStyle(container) padX = parseFloat(containerStyle.paddingLeft) + parseFloat(containerStyle.paddingRight) padY = parseFloat(containerStyle.paddingTop) + parseFloat(containerStyle.paddingBottom) } const contentWidth = Math.ceil(visual.width + padX) const contentHeight = Math.ceil(visual.height + padY) if (!doc.head?.querySelector('style[data-preview-outer-scroll]')) { const styleEl = doc.createElement('style') styleEl.setAttribute('data-preview-outer-scroll', 'true') styleEl.textContent = 'html, body { overflow: hidden !important; height: auto !important; }' doc.head?.appendChild(styleEl) } setIframeSize((prev) => { const width = `${contentWidth}px` const height = `${contentHeight}px` if (prev.width === width && prev.height === height) { return prev } return { width, height } }) }, []) const restoreScrollPosition = useCallback(() => { const scrollEl = scrollElementRef.current if (!scrollEl) { return } scrollEl.scrollTo(savedScrollRef.current.left, savedScrollRef.current.top) }, []) const handleIframeLoad = useCallback(() => { iframeSizeObserverRef.current?.disconnect() iframeSizeObserverRef.current = null if (previewType != 'HTML') { return } applyIframeContentSize() const doc = iframeRef.current?.contentDocument const observed = doc?.querySelector('.previewDocument') || doc?.querySelector('.previewContainer') || doc?.body if (observed && typeof ResizeObserver === 'function') { const observer = new ResizeObserver(() => { applyIframeContentSize() }) observer.observe(observed) iframeSizeObserverRef.current = observer } }, [applyIframeContentSize, previewType]) useLayoutEffect(() => { restoreScrollPosition() }, [iframeSize, restoreScrollPosition]) const updatePreviewContentHTML = (html) => { const scrollEl = scrollElementRef.current if (scrollEl) { savedScrollRef.current = { top: scrollEl.scrollTop, left: scrollEl.scrollLeft } } setPreviewContentHTML(html) } const reloadPreviewPDF = useCallback( (content, testObject = {}) => { const templateId = documentTemplate?._id if (!templateId) { return } const requestId = ++previewRequestIdRef.current setReloadLoading(true) fetchTemplatePDF(templateId, content, testObject, (result) => { if (requestId !== previewRequestIdRef.current) { return } setReloadLoading(false) if (result?.error) { onPreviewMessage(result.error, true) } else { const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' }) const pdfUrl = URL.createObjectURL(pdfBlob) setPDFBlob(pdfUrl) onPreviewMessage('No issues found.', false) } }) }, [documentTemplate?._id, fetchTemplatePDF, onPreviewMessage] ) const reloadPreview = useCallback( (content, testObject = {}, scale = 1) => { const templateId = documentTemplate?._id if (!templateId) { return } const requestId = ++previewRequestIdRef.current setReloadLoading(true) fetchTemplatePreview(templateId, content, testObject, scale, (result) => { if (requestId !== previewRequestIdRef.current) { return } setReloadLoading(false) if (result?.error) { onPreviewMessage(result.error, true) } else { updatePreviewContentHTML(result.html) onPreviewMessage('No issues found.', false) } }) }, [fetchTemplatePreview, onPreviewMessage, documentTemplate?._id] ) const templateContent = documentTemplate?.content const templateId = documentTemplate?._id // Move useEffect to component level and use state to track objectData changes useEffect(() => { if (!templateId || !templateContent) { return } const timeoutId = window.setTimeout(() => { if (previewType == 'HTML') { reloadPreview(templateContent, objectData, previewScale) } else { reloadPreviewPDF(templateContent, objectData) } }, 300) return () => { window.clearTimeout(timeoutId) } }, [ objectData, templateContent, templateId, previewScale, previewType, reloadPreview, reloadPreviewPDF ]) useEffect(() => { return () => { iframeSizeObserverRef.current?.disconnect() } }, []) useEffect(() => { if (!panMode) { setIsPanning(false) } }, [panMode]) useEffect(() => { const scrollEl = scrollElementRef.current if (!panMode || !scrollEl || previewType != 'HTML') { return } const handlePointerDown = (event) => { if (event.pointerType === 'mouse' && event.button !== 0) { return } event.preventDefault() panStartRef.current = { x: event.clientX, y: event.clientY, scrollLeft: scrollEl.scrollLeft, scrollTop: scrollEl.scrollTop } setIsPanning(true) } scrollEl.addEventListener('pointerdown', handlePointerDown) return () => { scrollEl.removeEventListener('pointerdown', handlePointerDown) } }, [panMode, previewType]) useEffect(() => { if (!isPanning) { return } const previousCursor = document.body.style.cursor const previousUserSelect = document.body.style.userSelect document.body.style.cursor = 'grabbing' document.body.style.userSelect = 'none' const handlePointerMove = (event) => { const scrollEl = scrollElementRef.current if (!scrollEl) { return } const { x, y, scrollLeft, scrollTop } = panStartRef.current scrollEl.scrollLeft = scrollLeft - (event.clientX - x) scrollEl.scrollTop = scrollTop - (event.clientY - y) } const handlePointerUp = () => { setIsPanning(false) } window.addEventListener('pointermove', handlePointerMove) window.addEventListener('pointerup', handlePointerUp) window.addEventListener('pointercancel', handlePointerUp) return () => { document.body.style.cursor = previousCursor document.body.style.userSelect = previousUserSelect window.removeEventListener('pointermove', handlePointerMove) window.removeEventListener('pointerup', handlePointerUp) window.removeEventListener('pointercancel', handlePointerUp) } }, [isPanning]) return ( {showTestObject == true ? ( <> {documentTemplate?.objectType ? ( ) : (
)} {showPreviewSwitch == true ? (