javascript - How to create a scalable document element inside a container in HTML - Stack Overflow

admin2025-04-18  4

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:

  1. the document rect begins centered horizontally.
  2. if the document width is less than the screen no need to display horizontal bar.
  3. otherwise if document width is more than screen the horizontal scroolbar show up and allow movement on the X axys.
  4. the scrollbar position is scaled to keep what's at the center of the screen still at the center and makes the zoom nicer.

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:

  1. it requires to know the children size as props.
  2. the scale is not perfect because it doesn't take in account the whole rect of the children.
  3. I want to add some padding to prevent the scaled document to touch the borders of the screen, and still the math is not enough perfect.
  4. the scrollbar positions doesn't follow the zoom, this is very important.

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:

  1. the document rect begins centered horizontally.
  2. if the document width is less than the screen no need to display horizontal bar.
  3. otherwise if document width is more than screen the horizontal scroolbar show up and allow movement on the X axys.
  4. the scrollbar position is scaled to keep what's at the center of the screen still at the center and makes the zoom nicer.

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:

  1. it requires to know the children size as props.
  2. the scale is not perfect because it doesn't take in account the whole rect of the children.
  3. I want to add some padding to prevent the scaled document to touch the borders of the screen, and still the math is not enough perfect.
  4. the scrollbar positions doesn't follow the zoom, this is very important.

How could I allow this component to properly scale his content and allow the scrollbar position to follow the scale?

Share asked Jan 30 at 10:41 maggmagg 651 silver badge6 bronze badges 3
  • You should not use createRef inside a function component, the return result is not stable. Use useRef instead. Docs – rschristian Commented Jan 31 at 3:18
  • With preact I get a type error if with useRef yet useRef works as expected. idk if it's unseen by framework's devs but createRef works good enough. The error says tat useRef returns MutableRef instead of the Ref object which preact want. – magg Commented Jan 31 at 10:33
  • I am a framework dev and that type error comes from not initializing useRef (which you should be doing). createRef will cause bugs, it's absolutely not what you want. – rschristian Commented Jan 31 at 20:47
Add a comment  | 

2 Answers 2

Reset to default 1

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:

  • Updates when the client size of the root element changes.
  • Scaling only cares about the width.
  • Scales the scrollbar movement (not perfect but good enough).
  • Added padding to prevent the scaled document from touching the borders.
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>
    );
}

// **********************************************************
转载请注明原文地址:http://anycun.com/QandA/1744925474a89573.html