for a program I need to show inside a container a document (not the HTML document element) and this document rect as a fixed width and height and should allow zooming from the user. For me making this is very hard and I need help to create it.
This feature I want to make is the same as how MS Word and Libreoffice Writer scales the document. Here I've reverse engineered how it works:
I'm using the preact framework to make this app so I'll show the code of the component.
My first attempt I've tried to use CSS only with translate, but the horizontal scrollbar doesn't show the correct amount to scroll and half the document is not visible.
So I've attempted to add javascript for the calculation and I come up with this component:
import { createRef } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import type { JSX } from "preact/jsx-runtime";
export interface ContainerScrollScalableProps
{
width: number,
height: number,
scale: number,
children?: any,
}
export const ContainerScrollScalable = (props: ContainerScrollScalableProps): JSX.Element =>
{
const container = createRef<HTMLDivElement>();
const [containerW, setW] = useState<number>(0);
const [containerH, setH] = useState<number>(0);
useLayoutEffect(() =>
{
if (container.current)
{
setW(container.current.clientWidth);
setH(container.current.clientHeight);
console.log(container.current.scrollTop);
}
}, [container.current]);
const padTop = containerH * 0.2;
const padBottom = containerH * 0.4;
const padXs = 16 * props.scale;
const scaledW = (props.width * props.scale) + (padXs * 2);
const sizeW = containerW > scaledW ? containerW : scaledW;
return (
<div class="w-100 h-100 overflow-y-scroll overflow-x-auto d-flex" ref={container} >
<div class="border" style={"transform-origin: top center;"
+ `min-width: ${sizeW.toFixed(0)}px; min-height: ${props.height}px;`} >
<div style={`width: ${props.width}px; height: ${props.width}px;`
+ "transform-origin: top center;"
+ `transform: translate(${((sizeW / 2) - (props.width / 2) + padXs).toFixed(0)}px, ${(padTop).toFixed(0)}px) scale(${props.scale});`}
children={props.children} />
</div>
</div>
);
}
Then the component is used like this, will show a border so it doesn't need actual children elements to test it:
const zoom = getUserZoom(); // range 0.1 --> 3.0
// ...
<ContainerScrollScalable width={1080} height={1920} scale={zoom} />
This component has some problems:
How could I allow this component to properly scale his content and allow the scrollbar position to follow the scale?
for a program I need to show inside a container a document (not the HTML document element) and this document rect as a fixed width and height and should allow zooming from the user. For me making this is very hard and I need help to create it.
This feature I want to make is the same as how MS Word and Libreoffice Writer scales the document. Here I've reverse engineered how it works:
I'm using the preact framework to make this app so I'll show the code of the component.
My first attempt I've tried to use CSS only with translate, but the horizontal scrollbar doesn't show the correct amount to scroll and half the document is not visible.
So I've attempted to add javascript for the calculation and I come up with this component:
import { createRef } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import type { JSX } from "preact/jsx-runtime";
export interface ContainerScrollScalableProps
{
width: number,
height: number,
scale: number,
children?: any,
}
export const ContainerScrollScalable = (props: ContainerScrollScalableProps): JSX.Element =>
{
const container = createRef<HTMLDivElement>();
const [containerW, setW] = useState<number>(0);
const [containerH, setH] = useState<number>(0);
useLayoutEffect(() =>
{
if (container.current)
{
setW(container.current.clientWidth);
setH(container.current.clientHeight);
console.log(container.current.scrollTop);
}
}, [container.current]);
const padTop = containerH * 0.2;
const padBottom = containerH * 0.4;
const padXs = 16 * props.scale;
const scaledW = (props.width * props.scale) + (padXs * 2);
const sizeW = containerW > scaledW ? containerW : scaledW;
return (
<div class="w-100 h-100 overflow-y-scroll overflow-x-auto d-flex" ref={container} >
<div class="border" style={"transform-origin: top center;"
+ `min-width: ${sizeW.toFixed(0)}px; min-height: ${props.height}px;`} >
<div style={`width: ${props.width}px; height: ${props.width}px;`
+ "transform-origin: top center;"
+ `transform: translate(${((sizeW / 2) - (props.width / 2) + padXs).toFixed(0)}px, ${(padTop).toFixed(0)}px) scale(${props.scale});`}
children={props.children} />
</div>
</div>
);
}
Then the component is used like this, will show a border so it doesn't need actual children elements to test it:
const zoom = getUserZoom(); // range 0.1 --> 3.0
// ...
<ContainerScrollScalable width={1080} height={1920} scale={zoom} />
This component has some problems:
How could I allow this component to properly scale his content and allow the scrollbar position to follow the scale?
Makes proper scaling and centering with padding. Dynamic Sizing added Resizing Support adapts to window size.
import { createRef } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import type { JSX } from "preact/jsx-runtime";
export interface ScalableProps {
initialWidth: number;
initialHeight: number;
scale: number;
children?: any;
}
export const ScalableContainer = (props: ScalableProps): JSX.Element => {
const containerRef = createRef<HTMLDivElement>();
const [containerDims, setDims] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const update = () => {
if (containerRef.current)
setDims({
width: containerRef.current.clientWidth,
height: containerRef.current.clientHeight,
});
};
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
const scaledW = props.initialWidth * props.scale;
const scaledH = props.initialHeight * props.scale;
const padding = 16;
return (
<div
ref={containerRef}
class="w-100 h-100 overflow-scroll"
style={{ position: "relative" }}
>
<div
style={{
width: `${scaledW + padding * 2}px`,
height: `${scaledH + padding * 2}px`,
transform: `scale(${props.scale})`,
transformOrigin: "top center",
padding,
}}
>
<div
style={{
width: `${props.initialWidth}px`,
height: `${props.initialHeight}px`,
border: "1px solid #ccc",
background: "#f0f0f0",
}}
>
{props.children}
</div>
</div>
</div>
);
};
Example usage:
<ScalableContainer initialWidth={1080} initialHeight={1920} scale={1.5} />
Thanks @Rushil Mahadevu, I forgot yesterday to update here that I've found a solution with the following component and I'll share it as well.
similar to your answer and the previous snipped I've shared it comes with the following features:
import { createRef } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import type { JSX } from "preact/jsx-runtime";
// **********************************************************
export interface ContainerDocumentProps
{
docWidth: number,
docScale: number,
docPadX?: number,
children?: any,
}
// **********************************************************
export const ContainerDocument = (props: ContainerDocumentProps): JSX.Element =>
{
const clientRef = createRef<HTMLDivElement>();
const [areaWidth, setAreaWidth] = useState<number>(0);
const [docScale, setDocScale] = useState<number>(0.0);
const [userScollWidth, setUserScrollWidth] = useState<number>(0.5);
const onUpdateScale = (el: HTMLElement): void =>
{
const clientWidth = el.clientWidth;
const docPadX = props.docPadX ?? 0;
const docWidth = clientWidth * 0.75;
const docScale = (docWidth * props.docScale) / props.docWidth;
const areaScaled = Math.max(clientWidth, (props.docWidth + docPadX) * docScale);
el.scrollLeft = (areaScaled - clientWidth) * userScollWidth;
setAreaWidth(areaScaled);
setDocScale(docScale);
}
const onUpdateScroll = (ev: Event): void =>
{
const target = ev.target as HTMLDivElement;
setUserScrollWidth(target.scrollLeft / (target.scrollWidth - target.clientWidth));
}
useLayoutEffect(() =>
{
const el = clientRef.current;
if (el)
{
const observer = new ResizeObserver(() => onUpdateScale(el));
onUpdateScale(el);
observer.observe(el);
return () => observer.disconnect();
}
}, [clientRef.current, props.docScale]);
return (
<div class="w-100 h-100 overflow-y-scroll overflow-x-auto d-flex"
style="padding-top: 10vh; padding-bottom: 60vh;" ref={clientRef} onScroll={onUpdateScroll} >
<div class="position-relative d-flex flex-column" style={""
+ `min-width: ${areaWidth.toFixed(0)}px; height: fit-content;`} >
<div class="position-absolute top-0 start-50" style={"transform-origin: top center;"
+ `min-width: ${props.docWidth.toFixed(0)}px;`
+ `transform: translateX(-50%) scale(${docScale.toFixed(2)});`}
children={props.children} />
</div>
</div>
);
}
// **********************************************************
createRef
inside a function component, the return result is not stable. UseuseRef
instead. Docs – rschristian Commented Jan 31 at 3:18useRef
(which you should be doing).createRef
will cause bugs, it's absolutely not what you want. – rschristian Commented Jan 31 at 20:47