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 PropTypes from 'prop-types'
import { Flex, Button, Input, Select } from 'antd' import { Flex, Button, Input, Select } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx' import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx' import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx' import ObjectProperty from '../common/ObjectProperty.jsx'
import ScrollBox from './ScrollBox.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx' import { ApiServerContext } from '../context/ApiServerContext.jsx'
const noop = () => {} const noop = () => {}
@ -20,6 +28,9 @@ const TemplatePreview = ({
showPreviewSwitch = true showPreviewSwitch = true
}) => { }) => {
const iframeRef = useRef(null) const iframeRef = useRef(null)
const scrollElementRef = useRef(null)
const savedScrollRef = useRef({ top: 0, left: 0 })
const iframeSizeObserverRef = useRef(null)
const previewRequestIdRef = useRef(0) const previewRequestIdRef = useRef(0)
const { fetchTemplatePreview, fetchTemplatePDF } = const { fetchTemplatePreview, fetchTemplatePDF } =
useContext(ApiServerContext) useContext(ApiServerContext)
@ -28,23 +39,112 @@ const TemplatePreview = ({
const [reloadLoading, setReloadLoading] = useState(false) const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1) const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML') 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) => { const updatePreviewContentHTML = (html) => {
if (iframeRef.current) { const scrollEl = scrollElementRef.current
// Save current scroll position if (scrollEl) {
const scrollY = iframeRef.current.contentWindow.scrollY savedScrollRef.current = {
const scrollX = iframeRef.current.contentWindow.scrollX top: scrollEl.scrollTop,
left: scrollEl.scrollLeft
// 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)
} }
iframeRef.current.addEventListener('load', handleLoad)
} }
setPreviewContentHTML(html)
} }
const reloadPreviewPDF = useCallback( const reloadPreviewPDF = useCallback(
@ -84,24 +184,18 @@ const TemplatePreview = ({
const requestId = ++previewRequestIdRef.current const requestId = ++previewRequestIdRef.current
setReloadLoading(true) setReloadLoading(true)
fetchTemplatePreview( fetchTemplatePreview(templateId, content, testObject, scale, (result) => {
templateId, if (requestId !== previewRequestIdRef.current) {
content, return
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)
}
} }
) setReloadLoading(false)
if (result?.error) {
onPreviewMessage(result.error, true)
} else {
updatePreviewContentHTML(result.html)
onPreviewMessage('No issues found.', false)
}
})
}, },
[fetchTemplatePreview, onPreviewMessage, documentTemplate?._id] [fetchTemplatePreview, onPreviewMessage, documentTemplate?._id]
) )
@ -136,6 +230,12 @@ const TemplatePreview = ({
reloadPreviewPDF reloadPreviewPDF
]) ])
useEffect(() => {
return () => {
iframeSizeObserverRef.current?.disconnect()
}
}, [])
return ( return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}> <Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Flex gap={'small'}> <Flex gap={'small'}>
@ -149,6 +249,7 @@ const TemplatePreview = ({
objectData={objectData} objectData={objectData}
disabled={!isEditing || objectData?.global} disabled={!isEditing || objectData?.global}
type={'object'} type={'object'}
style={{ minWidth: 0 }}
/> />
) : ( ) : (
<div style={{ flexGrow: 1 }}> <div style={{ flexGrow: 1 }}>
@ -205,18 +306,50 @@ const TemplatePreview = ({
) : null} ) : null}
</Flex> </Flex>
<iframe <div
ref={iframeRef}
srcDoc={previewType == 'HTML' ? previewContentHTML : undefined}
src={previewType == 'PDF' ? pdfBlob : undefined}
frameBorder='0'
style={{ style={{
width: '100%', flex: '1 1 auto',
flexGrow: 1, minHeight: 0,
border: '1px solid #85858541', border: '1px solid #85858541'
overflow: 'auto'
}} }}
/> >
{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> </Flex>
) )
} }