Tried making my custom lightbox but it's hard man

This commit is contained in:
DrMint 2022-08-14 10:10:48 +02:00
parent eaef34a766
commit 9a8608a8e3
2 changed files with 175 additions and 2 deletions

View File

@ -1,10 +1,13 @@
import { Dispatch, SetStateAction, useCallback } from "react";
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import Hotkeys from "react-hot-keys";
import { useSwipeable } from "react-swipeable";
import { Img } from "./Img";
import { Button } from "./Inputs/Button";
import { Popup } from "./Popup";
import { Icon } from "components/Ico";
import { clamp } from "helpers/numbers";
import { cIf, cJoin } from "helpers/className";
import { useElementSize } from "hooks/useElementSize";
/*
*
@ -12,6 +15,11 @@ import { Icon } from "components/Ico";
*/
const SENSIBILITY_SWIPE = 0.5;
const TRANSLATION_PADDING = 100;
const SCALE_MAX = 5;
const SCALE_ON_DOUBLE_CLICK = 2;
const IMGWIDTH = 876;
const IMGHEIGHT = 1247;
/*
*
@ -56,6 +64,35 @@ export const LightBox = ({
},
});
const [scale, setScale] = useState(1);
const [translation, setTranslation] = useState({ x: 0, y: 0 });
const [isTranslating, setIsTranslating] = useState(false);
const [imgContainerRef, { width: containerWidth, height: containerHeight }] =
useElementSize();
const [imgRef, { width: imgWidth, height: imgHeight }] = useElementSize();
const changeTranslation = useCallback(
(movementX: number, movementY: number) => {
const diffX =
Math.abs(containerWidth - IMGWIDTH * scale) - TRANSLATION_PADDING;
const diffY =
Math.abs(containerHeight - IMGHEIGHT * scale) + TRANSLATION_PADDING;
setTranslation((current) => ({
x: clamp(current.x + movementX, -diffX / 2, diffX / 2),
y: clamp(current.y + movementY, -diffY / 2, diffY / 2),
}));
},
[containerHeight, containerWidth, scale]
);
const changeScale = useCallback(
(deltaY: number) =>
setScale((current) =>
clamp(current * (deltaY > 0 ? 0.9 : 1.1), 1, SCALE_MAX)
),
[]
);
return (
<>
{state && (
@ -81,6 +118,28 @@ export const LightBox = ({
className={`grid h-full w-full grid-cols-[4em,1fr,4em] place-items-center
overflow-hidden [grid-template-areas:"left_image_right"] first-letter:gap-4
mobile:grid-cols-2 mobile:[grid-template-areas:"image_image""left_right"]`}
ref={imgContainerRef}
onDragStart={(event) => event.preventDefault()}
onPointerDown={() => setIsTranslating(true)}
onPointerUp={() => setIsTranslating(false)}
onPointerMove={(event) => {
if (isTranslating) {
event.preventDefault();
changeTranslation(event.movementX, event.movementY);
}
}}
onWheel={(event) => {
changeScale(event.deltaY);
changeTranslation(0, 0);
}}
onDoubleClick={() => {
if (scale === 1) {
setScale(SCALE_ON_DOUBLE_CLICK);
} else {
setScale(1);
setTranslation({ x: 0, y: 0 });
}
}}
>
<div className="[grid-area:left]">
{index > 0 && (
@ -89,7 +148,16 @@ export const LightBox = ({
</div>
<Img
className="max-h-full min-h-fit [grid-area:image]"
ref={imgRef}
className={cJoin(
"max-h-full min-h-fit origin-center [grid-area:image]",
cIf(!isTranslating, "transition-transform")
)}
style={{
transform: `scale(${scale}) translate(${
translation.x / scale
}px, ${translation.y / scale}px)`,
}}
src={images[index]}
/>
@ -98,6 +166,24 @@ export const LightBox = ({
<Button onClick={handleNext} icon={Icon.ChevronRight} />
)}
</div>
<div className="absolute left-2 top-2 z-10 bg-light p-4">
<p>
Scale:{" "}
{scale.toLocaleString(undefined, {
maximumSignificantDigits: 3,
})}
</p>
<p>
Translation: {translation.x} {translation.y}
</p>
<p>
Container: {containerWidth}px {containerHeight}px
</p>
<p>
Image: {imgWidth}px {imgHeight}px
</p>
</div>
</div>
</Popup>
</Hotkeys>

View File

@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-invalid-void-type */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import {
RefObject,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
interface Size {
width: number;
height: number;
}
export const useElementSize = <T extends HTMLElement = HTMLDivElement>(): [
(node: T | null) => void,
Size
] => {
const [ref, setRef] = useState<T | null>(null);
const [size, setSize] = useState<Size>({
width: 0,
height: 0,
});
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
setSize({
width: ref?.offsetWidth || 0,
height: ref?.offsetHeight || 0,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref?.offsetHeight, ref?.offsetWidth]);
useEventListener("resize", handleSize);
useIsomorphicLayoutEffect(() => {
handleSize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref?.offsetHeight, ref?.offsetWidth]);
return [setRef, size];
};
export const useEventListener = <
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
T extends HTMLElement | void = void
>(
eventName: KW | KH,
handler: (
event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event
) => void,
element?: RefObject<T>,
options?: boolean | AddEventListenerOptions
) => {
// Create a ref that stores handler
const savedHandler = useRef(handler);
useIsomorphicLayoutEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current || window;
if (!(targetElement && targetElement.addEventListener)) {
return;
}
// Create event listener that calls handler function stored in ref
const eventListener: typeof handler = (event) =>
savedHandler.current(event);
targetElement.addEventListener(eventName, eventListener, options);
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener);
};
}, [eventName, element, options]);
};
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;