Tried making my custom lightbox but it's hard man
This commit is contained in:
parent
eaef34a766
commit
9a8608a8e3
|
@ -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>
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue