diff --git a/src/components/Dashboard/common/TemplatePreview.jsx b/src/components/Dashboard/common/TemplatePreview.jsx index 5a136b6a..73bcced0 100644 --- a/src/components/Dashboard/common/TemplatePreview.jsx +++ b/src/components/Dashboard/common/TemplatePreview.jsx @@ -1,10 +1,18 @@ -import { useState, useContext, useEffect, useRef, useCallback } from 'react' +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 ObjectProperty from '../common/ObjectProperty.jsx' +import ScrollBox from './ScrollBox.jsx' import { ApiServerContext } from '../context/ApiServerContext.jsx' const noop = () => {} @@ -20,6 +28,9 @@ const TemplatePreview = ({ 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) @@ -28,23 +39,112 @@ const TemplatePreview = ({ const [reloadLoading, setReloadLoading] = useState(false) const [previewScale, setPreviewScale] = useState(1) const [previewType, setPreviewType] = useState('HTML') + 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) => { - if (iframeRef.current) { - // Save current scroll position - const scrollY = iframeRef.current.contentWindow.scrollY - const scrollX = iframeRef.current.contentWindow.scrollX - - // Update srcDoc - setPreviewContentHTML(html) - - // Restore scroll position after iframe loads new content - const handleLoad = () => { - iframeRef.current.contentWindow.scrollTo(scrollX, scrollY) - iframeRef.current.removeEventListener('load', handleLoad) + const scrollEl = scrollElementRef.current + if (scrollEl) { + savedScrollRef.current = { + top: scrollEl.scrollTop, + left: scrollEl.scrollLeft } - iframeRef.current.addEventListener('load', handleLoad) } + setPreviewContentHTML(html) } const reloadPreviewPDF = useCallback( @@ -84,24 +184,18 @@ const TemplatePreview = ({ 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(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] ) @@ -136,6 +230,12 @@ const TemplatePreview = ({ reloadPreviewPDF ]) + useEffect(() => { + return () => { + iframeSizeObserverRef.current?.disconnect() + } + }, []) + return ( @@ -149,6 +249,7 @@ const TemplatePreview = ({ objectData={objectData} disabled={!isEditing || objectData?.global} type={'object'} + style={{ minWidth: 0 }} /> ) : (
@@ -205,18 +306,50 @@ const TemplatePreview = ({ ) : null} -