import { useState, useContext, useEffect, useLayoutEffect, useRef, useCallback } from 'react' import PropTypes from 'prop-types' import { Flex, Button, Input, InputNumber, Select, Card, Popover } 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 PDFViewer from './PDFViewer.jsx' import ScrollBox from './ScrollBox.jsx' import { ApiServerContext } from '../context/ApiServerContext.jsx' import ProgressDisplay from './ProgressDisplay.jsx' import { CaretDownFilled } from '@ant-design/icons' const noop = () => {} const TemplatePreview = ({ objectData, documentTemplate, content = null, template, templateType = 'documentTemplate', capabilities = {}, loading, isEditing, onTestObjectOpen = noop, onPreviewMessage = noop, showTestObject = false, showPreviewSwitch = true }) => { const currentTemplate = template || documentTemplate const htmlOnly = capabilities.htmlOnly === true const showWidthControl = capabilities.widthControl === true const defaultPreviewWidth = capabilities.defaultWidth || 600 const iframeRef = useRef(null) const scrollElementRef = useRef(null) const savedScrollRef = useRef({ top: 0, left: 0 }) const iframeSizeObserverRef = useRef(null) const previewRequestIdRef = useRef(0) const renderUnsubscribeRef = useRef(null) const { fetchTemplatePreview, fetchTemplateDownload, subscribeToObjectEvent } = useContext(ApiServerContext) const [previewContentHTML, setPreviewContentHTML] = useState('') const [pdfBlob, setPDFBlob] = useState(null) const [reloadLoading, setReloadLoading] = useState(false) const [downloadProgress, setDownloadProgress] = useState(null) const [previewScale, setPreviewScale] = useState(1) const [previewType, setPreviewType] = useState('HTML') const [previewWidth, setPreviewWidth] = useState(defaultPreviewWidth) 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 toolbarRef = useRef(null) const [usePopoverSettings, setUsePopoverSettings] = useState(false) useLayoutEffect(() => { const toolbar = toolbarRef.current if (!toolbar) return undefined const updateLayout = () => { const { width, height } = toolbar.getBoundingClientRect() console.log('Preview card size', { width, height }) setUsePopoverSettings(width < 620) } updateLayout() const observer = new ResizeObserver(updateLayout) observer.observe(toolbar) return () => observer.disconnect() }, []) const applyIframeContentSize = useCallback(() => { const iframe = iframeRef.current const doc = iframe?.contentDocument if (!iframe || !doc?.documentElement) { return } const wrappers = doc.querySelectorAll('.previewWrapper') wrappers.forEach((wrapper) => { const page = wrapper.querySelector('.previewDocument') if (!page) { return } const pageVisual = page.getBoundingClientRect() if (pageVisual.width <= 0 && pageVisual.height <= 0) { return } wrapper.style.width = `${pageVisual.width}px` wrapper.style.height = `${pageVisual.height}px` }) const documentEl = doc.querySelector('.previewPages') || doc.querySelector('.previewDocument') || doc.querySelector('.previewWrapper') || doc.body const container = doc.querySelector('.previewContainer') const visual = documentEl.getBoundingClientRect() if (visual.width <= 0 && visual.height <= 0) { return } 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 = showWidthControl ? previewWidth : Math.ceil(visual.width + padX) const contentHeight = Math.ceil( Math.max(visual.height + padY, doc.documentElement.scrollHeight) ) 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 } }) }, [previewWidth, showWidthControl]) 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('.previewPages') || doc?.querySelector('.previewContainer') || doc?.querySelector('.previewDocument') || 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 clearIframeContents = useCallback(() => { iframeSizeObserverRef.current?.disconnect() iframeSizeObserverRef.current = null const iframe = iframeRef.current if (iframe) { iframe.srcdoc = '' } setPreviewContentHTML('') setIframeSize({ width: showWidthControl ? `${previewWidth}px` : '100%', height: 'auto' }) }, [previewWidth, showWidthControl]) const stopRenderProgressListener = useCallback(() => { if (typeof renderUnsubscribeRef.current === 'function') { renderUnsubscribeRef.current() } renderUnsubscribeRef.current = null }, []) const applyPdfResult = useCallback( (result, requestId) => { if (requestId !== previewRequestIdRef.current) { return } setReloadLoading(false) setDownloadProgress(null) stopRenderProgressListener() if (result?.error) { onPreviewMessage(result.error, true) return } setPDFBlob( new Blob([result.buffer || result.pdf], { type: result.mime || 'application/pdf' }) ) onPreviewMessage('No issues found.', false) }, [onPreviewMessage, stopRenderProgressListener] ) const reloadPreviewPDF = useCallback( (content, testObject = {}) => { const templateId = currentTemplate?._id if (!templateId) { return } const requestId = ++previewRequestIdRef.current stopRenderProgressListener() setPDFBlob(null) setReloadLoading(true) setDownloadProgress({ progress: 0, message: 'Starting render...' }) const startDownload = async () => { try { const startResult = await fetchTemplateDownload( templateId, content, testObject, 'pdf', { async: true } ) if (requestId !== previewRequestIdRef.current) { return } if (startResult?.error || !startResult?.renderRequestId) { applyPdfResult(startResult, requestId) return } const renderRequestId = startResult.renderRequestId const eventType = `render:${renderRequestId}` let fetched = false const fetchRenderedFile = async () => { if (fetched) { return true } try { const result = await fetchTemplateDownload( templateId, content, testObject, 'pdf', { renderRequestId } ) fetched = true applyPdfResult(result, requestId) return true } catch (err) { if (err?.response?.status === 404) { return false } applyPdfResult( { error: err?.message || 'Failed to download rendered template.' }, requestId ) return true } } const unsubscribe = subscribeToObjectEvent( renderRequestId, 'render', eventType, (event) => { if (requestId !== previewRequestIdRef.current) { return } setDownloadProgress({ progress: Number(event?.progress) || 0, message: event?.message || '' }) if (event?.error) { applyPdfResult({ error: event.error }, requestId) return } if (Number(event?.progress) >= 1) { fetchRenderedFile() } } ) const pollId = window.setInterval(async () => { if (requestId !== previewRequestIdRef.current) { window.clearInterval(pollId) return } const didFetch = await fetchRenderedFile() if (didFetch) { window.clearInterval(pollId) } }, 1000) renderUnsubscribeRef.current = () => { window.clearInterval(pollId) if (typeof unsubscribe === 'function') { unsubscribe() } } } catch (err) { applyPdfResult( { error: err?.message || 'Failed to start template download.' }, requestId ) } } startDownload() }, [ applyPdfResult, currentTemplate?._id, fetchTemplateDownload, stopRenderProgressListener, subscribeToObjectEvent ] ) const reloadPreview = useCallback( (content, testObject = {}, scale = 1) => { const templateId = currentTemplate?._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) } }, templateType, showWidthControl ? { width: previewWidth } : {} ) }, [ currentTemplate?._id, fetchTemplatePreview, onPreviewMessage, previewWidth, showWidthControl, templateType ] ) const templateContent = currentTemplate?.content const templateId = currentTemplate?._id useEffect(() => { if (content != null) { updatePreviewContentHTML(content) return } if (!templateId || !templateContent || previewType != 'HTML') { return } const timeoutId = window.setTimeout(() => { if (content == null) { reloadPreview(templateContent, objectData, previewScale) } }, 300) return () => { window.clearTimeout(timeoutId) } }, [ objectData, templateContent, templateId, previewScale, previewWidth, previewType, reloadPreview, content ]) useEffect(() => { if (!templateId || !templateContent || previewType != 'PDF') { return } setPDFBlob(null) const timeoutId = window.setTimeout(() => { reloadPreviewPDF(templateContent, objectData) }, 300) return () => { window.clearTimeout(timeoutId) } }, [objectData, templateContent, templateId, previewType, reloadPreviewPDF]) useEffect(() => { return () => { iframeSizeObserverRef.current?.disconnect() stopRenderProgressListener() } }, [stopRenderProgressListener]) 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]) const showProgressOverlay = previewType != 'HTML' && downloadProgress != null const toolbarContent = ( <>