javascripthtmlcssreactjsuser-interface

How to create a scalable document element inside a container in HTML


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?


Solution

  • 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} />