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:
parent
699e178fa1
commit
fede8af6aa
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user