Enhance TemplatePreview Component with Scroll and Iframe Resizing Features

- Refactored the TemplatePreview component to improve iframe content size handling and scrolling behavior.
- Introduced a new ScrollBox component to manage scroll position and enhance user experience.
- Implemented a ResizeObserver to dynamically adjust iframe size based on content, ensuring better display of HTML previews.
- Added cleanup logic for the observer to prevent memory leaks during component unmounting.
This commit is contained in:
Tom Butcher 2026-08-21 23:06:11 +01:00
parent 699e178fa1
commit fede8af6aa

View File

@ -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 (
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Flex gap={'small'}>
@ -149,6 +249,7 @@ const TemplatePreview = ({
objectData={objectData}
disabled={!isEditing || objectData?.global}
type={'object'}
style={{ minWidth: 0 }}
/>
) : (
<div style={{ flexGrow: 1 }}>
@ -205,18 +306,50 @@ const TemplatePreview = ({
) : null}
</Flex>
<iframe
ref={iframeRef}
srcDoc={previewType == 'HTML' ? previewContentHTML : undefined}
src={previewType == 'PDF' ? pdfBlob : undefined}
frameBorder='0'
<div
style={{
width: '100%',
flexGrow: 1,
border: '1px solid #85858541',
overflow: 'auto'
flex: '1 1 auto',
minHeight: 0,
border: '1px solid #85858541'
}}
/>
>
{previewType == 'HTML' ? (
<ScrollBox scrollableNodeProps={{ ref: scrollElementRef }}>
<div
style={{
width: iframeSize.width,
height: iframeSize.height
}}
>
<iframe
ref={iframeRef}
srcDoc={previewContentHTML}
onLoad={handleIframeLoad}
scrolling='no'
frameBorder='0'
style={{
width: '100%',
height: iframeSize.height === 'auto' ? 150 : '100%',
display: 'block',
border: 0,
overflow: 'hidden'
}}
/>
</div>
</ScrollBox>
) : (
<iframe
ref={iframeRef}
src={pdfBlob}
frameBorder='0'
style={{
width: '100%',
height: '100%',
border: 0
}}
/>
)}
</div>
</Flex>
)
}