This commit is contained in:
sezzim 2025-12-04 03:04:27 +02:00 committed by GitHub
commit 015ab38151
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 325 additions and 157 deletions

View file

@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, {
ForwardedRef,
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { import {
Button, Button,
Col, Col,
@ -69,7 +76,9 @@ const CLASSNAME_FOOTER_RIGHT = `${CLASSNAME_FOOTER}-right`;
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`; const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`; const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`; const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
const CLASSNAME_SWIPE = `${CLASSNAME_CAROUSEL}-swipe`;
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`; const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
const CLASSNAME_IMAGE_CONTAINER = `${CLASSNAME_CAROUSEL}-image-container`;
const CLASSNAME_NAVBUTTON = `${CLASSNAME}-navbutton`; const CLASSNAME_NAVBUTTON = `${CLASSNAME}-navbutton`;
const CLASSNAME_NAV = `${CLASSNAME}-nav`; const CLASSNAME_NAV = `${CLASSNAME}-nav`;
const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`; const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`;
@ -82,6 +91,108 @@ const MIN_ZOOM = 0.1;
const SCROLL_ZOOM_TIMEOUT = 250; const SCROLL_ZOOM_TIMEOUT = 250;
const ZOOM_NONE_EPSILON = 0.015; const ZOOM_NONE_EPSILON = 0.015;
interface ILightboxCarouselProps {
transition: string | null;
currentIndex: number;
images: ILightboxImage[];
displayMode: GQL.ImageLightboxDisplayMode;
lightboxSettings: GQL.ConfigImageLightboxInput | undefined;
resetPosition?: boolean;
zoom: number;
scrollAttemptsBeforeChange: number;
firstScroll: React.MutableRefObject<number | null>;
inScrollGroup: React.MutableRefObject<boolean>;
movingLeft: boolean;
updateZoom: (v: number) => void;
debouncedScrollReset: () => void;
handleLeft: () => void;
handleRight: () => void;
overrideTransition: (t: string) => void;
}
const LightboxCarousel = forwardRef(function (
{
transition,
currentIndex,
images,
displayMode,
lightboxSettings,
resetPosition,
zoom,
scrollAttemptsBeforeChange,
firstScroll,
inScrollGroup,
movingLeft,
updateZoom,
debouncedScrollReset,
handleLeft,
handleRight,
overrideTransition,
}: ILightboxCarouselProps,
carouselRef: ForwardedRef<HTMLDivElement>
) {
const [carouselShift, setCarouselShift] = useState(0);
function handleMoveCarousel(delta: number) {
overrideTransition(CLASSNAME_INSTANT);
setCarouselShift(carouselShift + delta);
}
function handleReleaseCarousel(
event: React.TouchEvent,
swipeDuration: number
) {
const cappedDuration = Math.max(50, Math.min(500, swipeDuration)) / 1000;
const adjustedShift = carouselShift / (2 * cappedDuration);
if (adjustedShift < -window.innerWidth / 2) {
handleRight();
} else if (adjustedShift > window.innerWidth / 2) {
handleLeft();
}
setCarouselShift(0);
overrideTransition(CLASSNAME_SWIPE);
}
return (
<div
className={CLASSNAME_CAROUSEL + (transition ? ` ${transition}` : "")}
style={{ left: `calc(${currentIndex * -100}vw + ${carouselShift}px)` }}
ref={carouselRef}
>
{images.map((image, i) => (
<div className={`${CLASSNAME_IMAGE_CONTAINER}`} key={image.paths.image}>
{i >= currentIndex - 1 && i <= currentIndex + 1 ? (
<LightboxImage
src={image.paths.image ?? ""}
width={image.visual_files?.[0]?.width ?? 0}
height={image.visual_files?.[0]?.height ?? 0}
displayMode={displayMode}
scaleUp={lightboxSettings?.scaleUp ?? false}
scrollMode={
lightboxSettings?.scrollMode ?? GQL.ImageLightboxScrollMode.Zoom
}
resetPosition={resetPosition}
zoom={i === currentIndex ? zoom : 1}
scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
firstScroll={firstScroll}
inScrollGroup={inScrollGroup}
current={i === currentIndex}
alignBottom={movingLeft}
setZoom={updateZoom}
debouncedScrollReset={debouncedScrollReset}
onLeft={handleLeft}
onRight={handleRight}
isVideo={isVideo(image.visual_files?.[0] ?? {})}
moveCarousel={handleMoveCarousel}
releaseCarousel={handleReleaseCarousel}
/>
) : undefined}
</div>
))}
</div>
);
});
interface IProps { interface IProps {
images: ILightboxImage[]; images: ILightboxImage[];
isVisible: boolean; isVisible: boolean;
@ -117,7 +228,7 @@ export const LightboxComponent: React.FC<IProps> = ({
const [index, setIndex] = useState<number | null>(null); const [index, setIndex] = useState<number | null>(null);
const [movingLeft, setMovingLeft] = useState(false); const [movingLeft, setMovingLeft] = useState(false);
const oldIndex = useRef<number | null>(null); const oldIndex = useRef<number | null>(null);
const [instantTransition, setInstantTransition] = useState(false); const [transition, setTransition] = useState<string | null>(null);
const [isSwitchingPage, setIsSwitchingPage] = useState(true); const [isSwitchingPage, setIsSwitchingPage] = useState(true);
const [isFullscreen, setFullscreen] = useState(false); const [isFullscreen, setFullscreen] = useState(false);
const [showOptions, setShowOptions] = useState(false); const [showOptions, setShowOptions] = useState(false);
@ -144,7 +255,6 @@ export const LightboxComponent: React.FC<IProps> = ({
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const overlayTarget = useRef<HTMLButtonElement | null>(null); const overlayTarget = useRef<HTMLButtonElement | null>(null);
const carouselRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null); const indicatorRef = useRef<HTMLDivElement | null>(null);
const navRef = useRef<HTMLDivElement | null>(null); const navRef = useRef<HTMLDivElement | null>(null);
const clearIntervalCallback = useRef<() => void>(); const clearIntervalCallback = useRef<() => void>();
@ -232,15 +342,15 @@ export const LightboxComponent: React.FC<IProps> = ({
} }
}, [isSwitchingPage, images, index]); }, [isSwitchingPage, images, index]);
const disableInstantTransition = useDebounce( const restoreTransition = useDebounce(() => setTransition(null), 400);
() => setInstantTransition(false),
400
);
const setInstant = useCallback(() => { const overrideTransition = useCallback(
setInstantTransition(true); (t: string) => {
disableInstantTransition(); setTransition(t);
}, [disableInstantTransition]); restoreTransition();
},
[restoreTransition]
);
useEffect(() => { useEffect(() => {
if (images.length < 2) return; if (images.length < 2) return;
@ -422,12 +532,12 @@ export const LightboxComponent: React.FC<IProps> = ({
const handleKey = useCallback( const handleKey = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.repeat && (e.key === "ArrowRight" || e.key === "ArrowLeft")) if (e.repeat && (e.key === "ArrowRight" || e.key === "ArrowLeft"))
setInstant(); overrideTransition(CLASSNAME_INSTANT);
if (e.key === "ArrowLeft") handleLeft(); if (e.key === "ArrowLeft") handleLeft();
else if (e.key === "ArrowRight") handleRight(); else if (e.key === "ArrowRight") handleRight();
else if (e.key === "Escape") close(); else if (e.key === "Escape") close();
}, },
[setInstant, handleLeft, handleRight, close] [overrideTransition, handleLeft, handleRight, close]
); );
const handleFullScreenChange = () => { const handleFullScreenChange = () => {
if (clearIntervalCallback.current) { if (clearIntervalCallback.current) {
@ -869,45 +979,24 @@ export const LightboxComponent: React.FC<IProps> = ({
<Icon icon={faChevronLeft} /> <Icon icon={faChevronLeft} />
</Button> </Button>
)} )}
<LightboxCarousel
<div transition={transition}
className={cx(CLASSNAME_CAROUSEL, { currentIndex={currentIndex}
[CLASSNAME_INSTANT]: instantTransition, images={images}
})} displayMode={displayMode}
style={{ left: `${currentIndex * -100}vw` }} lightboxSettings={lightboxSettings}
ref={carouselRef} resetPosition={resetPosition}
> zoom={zoom}
{images.map((image, i) => ( scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
<div className={`${CLASSNAME_IMAGE}`} key={image.paths.image}> firstScroll={firstScroll}
{i >= currentIndex - 1 && i <= currentIndex + 1 ? ( inScrollGroup={inScrollGroup}
<LightboxImage movingLeft={movingLeft}
src={image.paths.image ?? ""} updateZoom={updateZoom}
width={image.visual_files?.[0]?.width ?? 0} debouncedScrollReset={debouncedScrollReset}
height={image.visual_files?.[0]?.height ?? 0} handleLeft={handleLeft}
displayMode={displayMode} handleRight={handleRight}
scaleUp={lightboxSettings?.scaleUp ?? false} overrideTransition={overrideTransition}
scrollMode={ />
lightboxSettings?.scrollMode ??
GQL.ImageLightboxScrollMode.Zoom
}
resetPosition={resetPosition}
zoom={i === currentIndex ? zoom : 1}
scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
firstScroll={firstScroll}
inScrollGroup={inScrollGroup}
current={i === currentIndex}
alignBottom={movingLeft}
setZoom={updateZoom}
debouncedScrollReset={debouncedScrollReset}
onLeft={handleLeft}
onRight={handleRight}
isVideo={isVideo(image.visual_files?.[0] ?? {})}
/>
) : undefined}
</div>
))}
</div>
{allowNavigation && ( {allowNavigation && (
<Button <Button
variant="link" variant="link"

View file

@ -1,5 +1,13 @@
import React, { useEffect, useRef, useState, useCallback } from "react"; import React, {
MutableRefObject,
useEffect,
useRef,
useState,
useCallback,
} from "react";
import useResizeObserver from "@react-hook/resize-observer";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import cx from "classnames";
const ZOOM_STEP = 1.1; const ZOOM_STEP = 1.1;
const ZOOM_FACTOR = 700; const ZOOM_FACTOR = 700;
@ -11,6 +19,7 @@ const SCROLL_PAN_FACTOR = 2;
const CLASSNAME = "Lightbox"; const CLASSNAME = "Lightbox";
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`; const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`; const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
const CLASSNAME_IMAGE_PAN = `${CLASSNAME_IMAGE}-pan`;
function calculateDefaultZoom( function calculateDefaultZoom(
width: number, width: number,
@ -50,6 +59,28 @@ function calculateDefaultZoom(
return newZoom; return newZoom;
} }
interface IDimension {
width: number;
height: number;
}
export const useContainerDimensions = <
T extends HTMLElement = HTMLDivElement
>(): [MutableRefObject<T | null>, IDimension] => {
const target = useRef<T | null>(null);
const [dimension, setDimension] = useState<IDimension>({
width: 0,
height: 0,
});
useResizeObserver(target, (entry) => {
const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0];
setDimension({ width, height });
});
return [target, dimension];
};
interface IProps { interface IProps {
src: string; src: string;
width: number; width: number;
@ -71,6 +102,8 @@ interface IProps {
debouncedScrollReset: () => void; debouncedScrollReset: () => void;
onLeft: () => void; onLeft: () => void;
onRight: () => void; onRight: () => void;
moveCarousel: (v: number) => void;
releaseCarousel: (ev: React.TouchEvent, swipeDuration: number) => void;
isVideo: boolean; isVideo: boolean;
} }
@ -92,38 +125,34 @@ export const LightboxImage: React.FC<IProps> = ({
debouncedScrollReset, debouncedScrollReset,
onLeft, onLeft,
onRight, onRight,
moveCarousel,
releaseCarousel,
isVideo, isVideo,
}) => { }) => {
const [defaultZoom, setDefaultZoom] = useState(1); const [defaultZoom, setDefaultZoom] = useState<number | null>(null);
const [moving, setMoving] = useState(false); const [moving, setMoving] = useState(false);
const [positionX, setPositionX] = useState(0); const [positionX, setPositionX] = useState(0);
const [positionY, setPositionY] = useState(0); const [positionY, setPositionY] = useState(0);
const [imageWidth, setImageWidth] = useState(width); const [imageWidth, setImageWidth] = useState(width);
const [imageHeight, setImageHeight] = useState(height); const [imageHeight, setImageHeight] = useState(height);
const [boxWidth, setBoxWidth] = useState(0);
const [boxHeight, setBoxHeight] = useState(0);
const dimensionsProvided = width > 0 && height > 0; const dimensionsProvided = width > 0 && height > 0;
const [containerRef, { width: boxWidth, height: boxHeight }] =
useContainerDimensions();
const mouseDownEvent = useRef<MouseEvent>(); const mouseDownEvent = useRef<MouseEvent>();
const resetPositionRef = useRef(resetPosition); const resetPositionRef = useRef(resetPosition);
const container = React.createRef<HTMLDivElement>();
const startPoints = useRef<number[]>([0, 0]); const startPoints = useRef<number[]>([0, 0]);
const startTime = useRef<number>(0);
const pointerCache = useRef<React.PointerEvent[]>([]); const pointerCache = useRef<React.PointerEvent[]>([]);
const prevDiff = useRef<number | undefined>(); const prevDiff = useRef<number | undefined>();
const scrollAttempts = useRef(0); const scrollAttempts = useRef(0);
useEffect(() => { useEffect(() => {
const box = container.current;
if (box) {
setBoxWidth(box.offsetWidth);
setBoxHeight(box.offsetHeight);
}
function toggleVideoPlay() { function toggleVideoPlay() {
if (container.current) { if (containerRef.current) {
let openVideo = container.current.getElementsByTagName("video"); let openVideo = containerRef.current.getElementsByTagName("video");
if (openVideo.length > 0) { if (openVideo.length > 0) {
let rect = openVideo[0].getBoundingClientRect(); let rect = openVideo[0].getBoundingClientRect();
if (Math.abs(rect.x) < document.body.clientWidth / 2) { if (Math.abs(rect.x) < document.body.clientWidth / 2) {
@ -138,7 +167,7 @@ export const LightboxImage: React.FC<IProps> = ({
setTimeout(() => { setTimeout(() => {
toggleVideoPlay(); toggleVideoPlay();
}, 250); }, 250);
}, [container]); }, [containerRef]);
useEffect(() => { useEffect(() => {
if (dimensionsProvided) { if (dimensionsProvided) {
@ -161,27 +190,30 @@ export const LightboxImage: React.FC<IProps> = ({
}; };
}, [src, dimensionsProvided]); }, [src, dimensionsProvided]);
const calcPanBounds = useCallback(
(appliedZoom: number) => {
const xRange = Math.max(appliedZoom * imageWidth - boxWidth, 0);
const yRange = Math.max(appliedZoom * imageHeight - boxHeight, 0);
const nonZero = xRange != 0 || yRange != 0;
return {
minX: -xRange / 2,
maxX: xRange / 2,
minY: -yRange / 2,
maxY: yRange / 2,
nonZero,
};
},
[imageWidth, boxWidth, imageHeight, boxHeight]
);
const panBounds =
defaultZoom !== null
? calcPanBounds(defaultZoom * zoom)
: { minX: 0, maxX: 0, minY: 0, maxY: 0, nonZero: false };
const minMaxY = useCallback( const minMaxY = useCallback(
(appliedZoom: number) => { (appliedZoom: number) => {
let minY, maxY: number; const minY = Math.min((boxHeight - appliedZoom * imageHeight) / 2, 0);
const inBounds = appliedZoom * imageHeight <= boxHeight; const maxY = Math.max((appliedZoom * imageHeight - boxHeight) / 2, 0);
// NOTE: I don't even know how these work, but they do
if (!inBounds) {
if (imageHeight > boxHeight) {
minY =
(appliedZoom * imageHeight - imageHeight) / 2 -
appliedZoom * imageHeight +
boxHeight;
maxY = (appliedZoom * imageHeight - imageHeight) / 2;
} else {
minY = (boxHeight - appliedZoom * imageHeight) / 2;
maxY = (appliedZoom * imageHeight - boxHeight) / 2;
}
} else {
minY = Math.min((boxHeight - imageHeight) / 2, 0);
maxY = minY;
}
return [minY, maxY]; return [minY, maxY];
}, },
@ -190,33 +222,21 @@ export const LightboxImage: React.FC<IProps> = ({
const calculateInitialPosition = useCallback( const calculateInitialPosition = useCallback(
(appliedZoom: number) => { (appliedZoom: number) => {
// Center image from container's center // If image is smaller than container, place in center. Otherwise, align
const newPositionX = Math.min((boxWidth - imageWidth) / 2, 0); // the left side of the image with the left side of the container, and
let newPositionY: number; // align either the top or bottom of the image with the corresponding
// edge of container, depending on whether navigation is forwards or
if (displayMode === GQL.ImageLightboxDisplayMode.FitXy) { // backwards.
newPositionY = Math.min((boxHeight - imageHeight) / 2, 0); const [minY, maxY] = minMaxY(appliedZoom);
} else { const newPositionX = Math.max(
// otherwise, align image with container (appliedZoom * imageWidth - boxWidth) / 2,
const [minY, maxY] = minMaxY(appliedZoom); 0
if (!alignBottom) { );
newPositionY = maxY; const newPositionY = alignBottom ? minY : maxY;
} else {
newPositionY = minY;
}
}
return [newPositionX, newPositionY]; return [newPositionX, newPositionY];
}, },
[ [boxWidth, imageWidth, alignBottom, minMaxY]
displayMode,
boxWidth,
imageWidth,
boxHeight,
imageHeight,
alignBottom,
minMaxY,
]
); );
useEffect(() => { useEffect(() => {
@ -267,6 +287,9 @@ export const LightboxImage: React.FC<IProps> = ({
]); ]);
useEffect(() => { useEffect(() => {
if (defaultZoom === null) {
return;
}
if (resetPosition !== resetPositionRef.current) { if (resetPosition !== resetPositionRef.current) {
resetPositionRef.current = resetPosition; resetPositionRef.current = resetPosition;
@ -352,7 +375,7 @@ export const LightboxImage: React.FC<IProps> = ({
} }
function onImageScrollPanY(ev: React.WheelEvent, infinite: boolean) { function onImageScrollPanY(ev: React.WheelEvent, infinite: boolean) {
if (!current) return; if (!current || defaultZoom === null) return;
const [minY, maxY] = minMaxY(zoom * defaultZoom); const [minY, maxY] = minMaxY(zoom * defaultZoom);
@ -386,6 +409,8 @@ export const LightboxImage: React.FC<IProps> = ({
} }
function onImageScroll(ev: React.WheelEvent) { function onImageScroll(ev: React.WheelEvent) {
if (defaultZoom === null) return;
const absDeltaY = Math.abs(ev.deltaY); const absDeltaY = Math.abs(ev.deltaY);
const firstDeltaY = firstScroll.current; const firstDeltaY = firstScroll.current;
// detect infinite scrolling (mousepad, mouse with infinite scrollwheel) // detect infinite scrolling (mousepad, mouse with infinite scrollwheel)
@ -405,6 +430,9 @@ export const LightboxImage: React.FC<IProps> = ({
percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP; percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
} }
setZoom(zoom * percent); setZoom(zoom * percent);
const bounds = calcPanBounds(defaultZoom * zoom * percent);
setPositionX(Math.max(bounds.minX, Math.min(bounds.maxX, positionX)));
setPositionY(Math.max(bounds.minY, Math.min(bounds.maxY, positionY)));
break; break;
case GQL.ImageLightboxScrollMode.PanY: case GQL.ImageLightboxScrollMode.PanY:
onImageScrollPanY(ev, infinite); onImageScrollPanY(ev, infinite);
@ -430,16 +458,26 @@ export const LightboxImage: React.FC<IProps> = ({
return; return;
} }
const posX = ev.pageX - startPoints.current[0]; const deltaX = ev.pageX - startPoints.current[0];
const posY = ev.pageY - startPoints.current[1]; const deltaY = ev.pageY - startPoints.current[1];
startPoints.current = [ev.pageX, ev.pageY]; startPoints.current = [ev.pageX, ev.pageY];
setPositionX(positionX + posX); const newPositionX = Math.max(
setPositionY(positionY + posY); panBounds.minX,
Math.min(panBounds.maxX, positionX + deltaX)
);
const newPositionY = Math.max(
panBounds.minY,
Math.min(panBounds.maxY, positionY + deltaY)
);
setPositionX(newPositionX);
setPositionY(newPositionY);
} }
function onImageMouseDown(ev: React.MouseEvent) { function onImageMouseDown(ev: React.MouseEvent) {
startPoints.current = [ev.pageX, ev.pageY]; startPoints.current = [ev.pageX, ev.pageY];
startTime.current = ev.timeStamp;
setMoving(true); setMoving(true);
mouseDownEvent.current = ev.nativeEvent; mouseDownEvent.current = ev.nativeEvent;
@ -471,24 +509,58 @@ export const LightboxImage: React.FC<IProps> = ({
} }
} }
function onTouchStart(ev: React.TouchEvent) { const onTouchStart = useCallback(
ev.preventDefault(); (ev: TouchEvent) => {
if (ev.touches.length === 1) { ev.preventDefault();
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY]; if (ev.touches.length === 1) {
setMoving(true); startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
startTime.current = ev.timeStamp;
setMoving(true);
}
},
[startPoints, startTime, setMoving]
);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
} }
} container.addEventListener("touchstart", onTouchStart);
return () => {
container.removeEventListener("touchstart", onTouchStart);
};
}, [containerRef, onTouchStart]);
function onTouchMove(ev: React.TouchEvent) { function onTouchMove(ev: React.TouchEvent) {
if (!moving) return; if (!moving) return;
if (ev.touches.length === 1) { if (ev.touches.length === 1) {
const posX = ev.touches[0].pageX - startPoints.current[0]; const deltaX = ev.touches[0].pageX - startPoints.current[0];
const posY = ev.touches[0].pageY - startPoints.current[1]; const deltaY = ev.touches[0].pageY - startPoints.current[1];
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY]; startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
setPositionX(positionX + posX); if (panBounds.minX != panBounds.maxX) {
setPositionY(positionY + posY); const newPositionX = Math.max(
panBounds.minX,
Math.min(panBounds.maxX, positionX + deltaX)
);
const newPositionY = Math.max(
panBounds.minY,
Math.min(panBounds.maxY, positionY + deltaY)
);
setPositionX(newPositionX);
setPositionY(newPositionY);
} else {
moveCarousel(deltaX);
}
}
}
function onTouchEnd(ev: React.TouchEvent) {
if (ev.changedTouches.length === 1 && ev.touches.length === 0) {
releaseCarousel(ev, ev.timeStamp - startTime.current);
} }
} }
@ -520,6 +592,8 @@ export const LightboxImage: React.FC<IProps> = ({
pointerCache.current[cachedIndex] = ev; pointerCache.current[cachedIndex] = ev;
} }
if (defaultZoom === null) return;
// compare the difference between the two pointers // compare the difference between the two pointers
if (pointerCache.current.length === 2) { if (pointerCache.current.length === 2) {
const ev1 = pointerCache.current[0]; const ev1 = pointerCache.current[0];
@ -532,11 +606,11 @@ export const LightboxImage: React.FC<IProps> = ({
const diffDiff = diff - prevDiff.current; const diffDiff = diff - prevDiff.current;
const factor = (Math.abs(diffDiff) / 20) * 0.1 + 1; const factor = (Math.abs(diffDiff) / 20) * 0.1 + 1;
if (diffDiff > 0) { let newZoom = diffDiff > 0 ? zoom * factor : zoom / factor;
setZoom(zoom * factor); setZoom(newZoom);
} else if (diffDiff < 0) { const bounds = calcPanBounds(defaultZoom * newZoom);
setZoom((zoom * 1) / factor); setPositionX(Math.max(bounds.minX, Math.min(bounds.maxX, positionX)));
} setPositionY(Math.max(bounds.minY, Math.min(bounds.maxY, positionY)));
} }
prevDiff.current = diff; prevDiff.current = diff;
@ -547,32 +621,36 @@ export const LightboxImage: React.FC<IProps> = ({
return ( return (
<div <div
ref={container} ref={containerRef}
className={`${CLASSNAME_IMAGE}`} className={cx(CLASSNAME_IMAGE, {
[CLASSNAME_IMAGE_PAN]: panBounds.nonZero,
})}
style={{ touchAction: "none" }}
onWheel={(e) => onContainerScroll(e)} onWheel={(e) => onContainerScroll(e)}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
> >
{defaultZoom ? ( {defaultZoom ? (
<picture <picture>
style={{
transform: `translate(${positionX}px, ${positionY}px) scale(${
defaultZoom * zoom
})`,
}}
>
<source srcSet={src} media="(min-width: 800px)" />
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<ImageView <ImageView
style={{
touchAction: "none",
position: "relative",
left: "50%",
top: "50%",
transform: `translate(-50%, -50%) translate(${positionX}px, ${positionY}px) scale(${
defaultZoom * zoom
})`,
}}
loop={isVideo} loop={isVideo}
src={src} src={src}
alt="" alt=""
draggable={false} draggable={false}
style={{ touchAction: "none" }}
onWheel={current ? (e) => onImageScroll(e) : undefined} onWheel={current ? (e) => onImageScroll(e) : undefined}
onMouseDown={onImageMouseDown} onMouseDown={onImageMouseDown}
onMouseUp={onImageMouseUp} onMouseUp={onImageMouseUp}
onMouseMove={onImageMouseOver} onMouseMove={onImageMouseOver}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onPointerUp={onPointerUp} onPointerUp={onPointerUp}
onPointerMove={onPointerMove} onPointerMove={onPointerMove}

View file

@ -144,28 +144,29 @@
transition-duration: 0ms; transition-duration: 0ms;
} }
&-image { &-swipe {
transition-duration: 200ms;
transition-timing-function: ease-out;
}
&-image-container {
content-visibility: auto; content-visibility: auto;
display: flex;
width: 100vw; width: 100vw;
}
picture { &-image {
display: flex; height: 100%;
margin: auto; width: 100%;
position: relative;
> div {
display: flex;
height: 100%;
position: absolute;
width: 100%;
}
}
img { img {
cursor: pointer;
object-fit: contain; object-fit: contain;
} }
&-pan {
img {
cursor: pointer;
}
}
} }
} }