基于Preact实现仿Office文档的可缩放滚动容器组件的优化咨询
基于Preact实现仿Office文档的可缩放滚动容器组件的优化咨询
特别理解你的需求——要做一个像Word/Writer那样的缩放滚动容器,核心是缩放时能保持当前视野中心不变,同时滚动条能正确适配,不会出现内容半截看不到的情况。你之前用CSS+JS的尝试遇到了几个实际问题,我来帮你一步步优化这个组件:
先对齐核心实现逻辑(和你拆解的Office规则完全匹配)
- 初始状态下文档在容器水平居中
- 缩放后若文档宽度超过容器,显示水平滚动条;反之自动隐藏
- 缩放时保持当前屏幕中心的内容依然在中心,让缩放交互更顺滑
- 自动适配子元素尺寸,不需要手动传入宽高参数
- 加入内边距避免缩放后的内容贴容器边缘
优化后的Preact组件代码
import { createRef } from "preact"; import { useLayoutEffect, useState } from "preact/hooks"; import type { JSX } from "preact/jsx-runtime"; export interface ContainerScrollScalableProps { scale: number; padding?: number; // 内容与容器边缘的间距,默认20px children?: JSX.Element; } export const ContainerScrollScalable = (props: ContainerScrollScalableProps): JSX.Element => { const containerRef = createRef<HTMLDivElement>(); const contentRef = createRef<HTMLDivElement>(); const [contentSize, setContentSize] = useState({ width: 0, height: 0 }); const { scale = 1, padding = 20 } = props; // 自动监听子元素尺寸变化,无需手动传入宽高 useLayoutEffect(() => { if (!contentRef.current) return; // 初始化获取子元素原始尺寸 const updateContentSize = () => { if (contentRef.current) { setContentSize({ width: contentRef.current.scrollWidth, height: contentRef.current.scrollHeight, }); } }; updateContentSize(); // 监听子元素动态尺寸变化(比如内容加载、元素 resize) const observer = new ResizeObserver(updateContentSize); observer.observe(contentRef.current); return () => observer.disconnect(); }, [props.children]); // 核心:缩放时保持视野中心不变的逻辑 useLayoutEffect(() => { if (!containerRef.current || !contentRef.current || contentSize.width === 0) return; const container = containerRef.current; // 1. 获取上一次缩放比例,计算当前视野的原生中心坐标(不受缩放影响) const prevScale = parseFloat(container.dataset.prevScale || "1"); const viewportCenterX = container.scrollLeft + container.clientWidth / 2; const viewportCenterY = container.scrollTop + container.clientHeight / 2; const nativeCenterX = viewportCenterX / prevScale; const nativeCenterY = viewportCenterY / prevScale; // 2. 根据新缩放比例计算新的中心坐标,调整滚动位置 const newCenterX = nativeCenterX * scale; const newCenterY = nativeCenterY * scale; container.scrollLeft = newCenterX - container.clientWidth / 2; container.scrollTop = newCenterY - container.clientHeight / 2; // 3. 记录当前缩放比例,供下一次缩放计算使用 container.dataset.prevScale = scale.toString(); }, [scale, contentSize]); // 计算缩放后的内容尺寸和容器滚动区域尺寸 const scaledContentWidth = contentSize.width * scale; const scaledContentHeight = contentSize.height * scale; const scrollableAreaWidth = scaledContentWidth + padding * 2; const scrollableAreaHeight = scaledContentHeight + padding * 2; // 计算内容的初始偏移:水平居中 + 左右padding const contentOffsetX = Math.max((containerRef.current?.clientWidth || 0) / 2 - scaledContentWidth / 2, 0) + padding; const contentOffsetY = padding; return ( <div ref={containerRef} className="w-100 h-100 overflow-auto" style={{ minWidth: `${scrollableAreaWidth}px`, minHeight: `${scrollableAreaHeight}px`, }} > {/* 外层容器:承载缩放后的内容,确保滚动范围完全覆盖内容+padding */} <div style={{ position: "relative", width: `${scrollableAreaWidth}px`, height: `${scrollableAreaHeight}px`, }} > {/* 实际内容容器:负责缩放和定位 */} <div ref={contentRef} style={{ position: "absolute", top: `${contentOffsetY}px`, left: `${contentOffsetX}px`, transformOrigin: "top left", // 用左上角作为变换原点,计算更直观 transform: `scale(${scale})`, width: `${contentSize.width}px`, height: `${contentSize.height}px`, }} > {props.children} </div> </div> </div> ); };
组件使用示例
const zoom = getUserZoom(); // 范围 0.1 ~ 3.0 // 直接传入缩放比例即可,无需手动传入子元素宽高 <ContainerScrollScalable scale={zoom} padding={24}> {/* 这里放你的文档内容,比如固定尺寸的容器或者任意子元素 */} <div style={{ width: 1080, height: 1920, border: "1px solid #ccc" }}> 你的文档内容 </div> </ContainerScrollScalable>
关键问题的解决说明
自动获取子元素尺寸
用ResizeObserver监听子元素的尺寸变化,无需手动在props里传入宽高,组件会自动适配子元素的实际大小,包括动态内容的尺寸变化。缩放时保持视野中心
先将当前视野的中心坐标转换为子元素的原生尺寸坐标(不受缩放影响),再根据新缩放比例计算新的中心位置,最后调整容器的scrollLeft/scrollTop,确保缩放前后中心内容位置不变。正确的滚动条适配
外层容器的minWidth/minHeight设置为缩放后的内容尺寸加上两倍padding,确保滚动条的滚动范围完全覆盖整个内容;用overflow-auto会自动根据内容尺寸显示/隐藏滚动条,完美匹配你需求里的“文档宽度小于容器时隐藏水平滚动条”规则。解决内容半截不可见的问题
用transformOrigin: "top left"作为变换原点,配合绝对定位的偏移,确保缩放后的内容的每个角落都能被滚动到;外层容器的尺寸完全覆盖缩放后的内容+padding,滚动条的范围100%准确。padding适配
新增paddingprops允许自定义内容与容器边缘的间距,且padding不会随缩放变化,确保内容始终不会贴容器边缘。
备注:内容来源于stack exchange,提问作者magg
相关产品推荐
相关产品推荐

