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 Hotkeys from "react-hot-keys";
|
||||||
import { useSwipeable } from "react-swipeable";
|
import { useSwipeable } from "react-swipeable";
|
||||||
import { Img } from "./Img";
|
import { Img } from "./Img";
|
||||||
import { Button } from "./Inputs/Button";
|
import { Button } from "./Inputs/Button";
|
||||||
import { Popup } from "./Popup";
|
import { Popup } from "./Popup";
|
||||||
import { Icon } from "components/Ico";
|
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{state && (
|
{state && (
|
||||||
|
@ -81,6 +118,28 @@ export const LightBox = ({
|
||||||
className={`grid h-full w-full grid-cols-[4em,1fr,4em] place-items-center
|
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
|
overflow-hidden [grid-template-areas:"left_image_right"] first-letter:gap-4
|
||||||
mobile:grid-cols-2 mobile:[grid-template-areas:"image_image""left_right"]`}
|
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]">
|
<div className="[grid-area:left]">
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
|
@ -89,7 +148,16 @@ export const LightBox = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Img
|
<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]}
|
src={images[index]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -98,6 +166,24 @@ export const LightBox = ({
|
||||||
<Button onClick={handleNext} icon={Icon.ChevronRight} />
|
<Button onClick={handleNext} icon={Icon.ChevronRight} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Hotkeys>
|
</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