Enhance TemplatePreview Component with Scaling and Layout Improvements
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- Introduced scaleCssSize function to dynamically adjust CSS sizes based on scaling factor.
- Added responsive width control for the preview document, improving layout adaptability.
- Implemented useLayoutEffect to apply iframe content size based on preview type and width.
- Refactored reloadPreview function to simplify parameters and ensure consistent width handling.
- Enhanced iframe rendering with scaling transformations for better visual representation.
This commit is contained in:
Tom Butcher 2026-09-12 23:47:57 +01:00
parent a590650d80
commit f01f978ec6

View File

@ -21,6 +21,17 @@ import ProgressDisplay from './ProgressDisplay.jsx'
import { CaretDownFilled } from '@ant-design/icons' import { CaretDownFilled } from '@ant-design/icons'
const noop = () => {} const noop = () => {}
const scaleCssSize = (size, scale) => {
if (scale === 1 || typeof size !== 'string' || !size.endsWith('px')) {
return size
}
const value = parseFloat(size)
if (!Number.isFinite(value)) {
return size
}
return `${Math.ceil(value * scale)}px`
}
const TemplatePreview = ({ const TemplatePreview = ({
objectData, objectData,
documentTemplate, documentTemplate,
@ -57,6 +68,8 @@ const TemplatePreview = ({
const [previewScale, setPreviewScale] = useState(1) const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML') const [previewType, setPreviewType] = useState('HTML')
const [previewWidth, setPreviewWidth] = useState(defaultPreviewWidth) const [previewWidth, setPreviewWidth] = useState(defaultPreviewWidth)
const previewWidthRef = useRef(previewWidth)
previewWidthRef.current = previewWidth
const [panMode, setPanMode] = useState(false) const [panMode, setPanMode] = useState(false)
const [isPanning, setIsPanning] = useState(false) const [isPanning, setIsPanning] = useState(false)
const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }) const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 })
@ -93,12 +106,43 @@ const TemplatePreview = ({
return return
} }
if (showWidthControl && !doc.querySelector('.previewDocument') && doc.body) {
const container = doc.createElement('div')
container.className = 'previewContainer'
const pages = doc.createElement('div')
pages.className = 'previewPages'
pages.id = 'pages'
const wrapper = doc.createElement('div')
wrapper.className = 'previewWrapper'
const page = doc.createElement('div')
page.className = 'previewDocument documentPage'
page.id = 'content'
while (doc.body.firstChild) {
page.appendChild(doc.body.firstChild)
}
wrapper.appendChild(page)
pages.appendChild(wrapper)
container.appendChild(pages)
doc.body.appendChild(container)
if (!doc.head?.querySelector('style[data-preview-chrome]')) {
const styleEl = doc.createElement('style')
styleEl.setAttribute('data-preview-chrome', 'true')
styleEl.textContent =
'.previewContainer{display:flex;justify-content:center;align-items:flex-start;padding:60px;box-sizing:border-box;width:fit-content}.previewPages{display:flex;flex-direction:row;align-items:flex-start;width:fit-content}.previewWrapper{position:relative;flex-shrink:0}.previewDocument{background:#fff;border:1px solid #000;box-shadow:0 0 5px rgba(0,0,0,.2);transform-origin:top left;position:relative;overflow:visible;box-sizing:content-box}'
doc.head?.appendChild(styleEl)
}
}
const wrappers = doc.querySelectorAll('.previewWrapper') const wrappers = doc.querySelectorAll('.previewWrapper')
wrappers.forEach((wrapper) => { wrappers.forEach((wrapper) => {
const page = wrapper.querySelector('.previewDocument') const page = wrapper.querySelector('.previewDocument')
if (!page) { if (!page) {
return return
} }
if (showWidthControl) {
page.style.width = `${previewWidth}px`
page.style.maxWidth = `${previewWidth}px`
}
const pageVisual = page.getBoundingClientRect() const pageVisual = page.getBoundingClientRect()
if (pageVisual.width <= 0 && pageVisual.height <= 0) { if (pageVisual.width <= 0 && pageVisual.height <= 0) {
return return
@ -130,9 +174,7 @@ const TemplatePreview = ({
parseFloat(containerStyle.paddingBottom) parseFloat(containerStyle.paddingBottom)
} }
const contentWidth = showWidthControl const contentWidth = Math.ceil(visual.width + padX)
? previewWidth
: Math.ceil(visual.width + padX)
const contentHeight = Math.ceil( const contentHeight = Math.ceil(
Math.max(visual.height + padY, doc.documentElement.scrollHeight) Math.max(visual.height + padY, doc.documentElement.scrollHeight)
) )
@ -192,6 +234,13 @@ const TemplatePreview = ({
restoreScrollPosition() restoreScrollPosition()
}, [iframeSize, restoreScrollPosition]) }, [iframeSize, restoreScrollPosition])
useLayoutEffect(() => {
if (previewType != 'HTML') {
return
}
applyIframeContentSize()
}, [applyIframeContentSize, previewType, previewWidth])
const updatePreviewContentHTML = (html) => { const updatePreviewContentHTML = (html) => {
const scrollEl = scrollElementRef.current const scrollEl = scrollElementRef.current
if (scrollEl) { if (scrollEl) {
@ -370,7 +419,7 @@ const TemplatePreview = ({
) )
const reloadPreview = useCallback( const reloadPreview = useCallback(
(content, testObject = {}, scale = 1) => { (content, testObject = {}) => {
const templateId = currentTemplate?._id const templateId = currentTemplate?._id
if (!templateId) { if (!templateId) {
return return
@ -382,7 +431,7 @@ const TemplatePreview = ({
templateId, templateId,
content, content,
testObject, testObject,
scale, 1,
(result) => { (result) => {
if (requestId !== previewRequestIdRef.current) { if (requestId !== previewRequestIdRef.current) {
return return
@ -396,14 +445,13 @@ const TemplatePreview = ({
} }
}, },
templateType, templateType,
showWidthControl ? { width: previewWidth } : {} showWidthControl ? { width: previewWidthRef.current } : {}
) )
}, },
[ [
currentTemplate?._id, currentTemplate?._id,
fetchTemplatePreview, fetchTemplatePreview,
onPreviewMessage, onPreviewMessage,
previewWidth,
showWidthControl, showWidthControl,
templateType templateType
] ]
@ -424,7 +472,7 @@ const TemplatePreview = ({
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
if (content == null) { if (content == null) {
reloadPreview(templateContent, objectData, previewScale) reloadPreview(templateContent, objectData)
} }
}, 300) }, 300)
@ -435,8 +483,6 @@ const TemplatePreview = ({
objectData, objectData,
templateContent, templateContent,
templateId, templateId,
previewScale,
previewWidth,
previewType, previewType,
reloadPreview, reloadPreview,
content content
@ -534,6 +580,8 @@ const TemplatePreview = ({
}, [isPanning]) }, [isPanning])
const showProgressOverlay = previewType != 'HTML' && downloadProgress != null const showProgressOverlay = previewType != 'HTML' && downloadProgress != null
const scaledIframeWidth = scaleCssSize(iframeSize.width, previewScale)
const scaledIframeHeight = scaleCssSize(iframeSize.height, previewScale)
const toolbarContent = ( const toolbarContent = (
<> <>
@ -707,14 +755,14 @@ const TemplatePreview = ({
align='center' align='center'
style={{ style={{
minWidth: '100%', minWidth: '100%',
width: iframeSize.width, width: scaledIframeWidth,
height: iframeSize.height height: scaledIframeHeight
}} }}
> >
<div <div
style={{ style={{
width: iframeSize.width, width: scaledIframeWidth,
height: iframeSize.height, height: scaledIframeHeight,
cursor: panMode cursor: panMode
? isPanning ? isPanning
? 'grabbing' ? 'grabbing'
@ -729,11 +777,14 @@ const TemplatePreview = ({
scrolling='no' scrolling='no'
frameBorder='0' frameBorder='0'
style={{ style={{
width: '100%', width:
height: iframeSize.height === 'auto' ? 150 : '100%', iframeSize.width === '100%' ? '100%' : iframeSize.width,
height: iframeSize.height === 'auto' ? 150 : iframeSize.height,
display: 'block', display: 'block',
border: 0, border: 0,
overflow: 'hidden', overflow: 'hidden',
transform: `scale(${previewScale})`,
transformOrigin: 'top left',
pointerEvents: panMode ? 'none' : undefined pointerEvents: panMode ? 'none' : undefined
}} }}
/> />