Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 141 additions & 52 deletions ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
ForwardedRef,
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import {
Button,
Col,
Expand Down Expand Up @@ -69,7 +76,9 @@ const CLASSNAME_FOOTER_RIGHT = `${CLASSNAME_FOOTER}-right`;
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
const CLASSNAME_SWIPE = `${CLASSNAME_CAROUSEL}-swipe`;
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
const CLASSNAME_IMAGE_CONTAINER = `${CLASSNAME_CAROUSEL}-image-container`;
const CLASSNAME_NAVBUTTON = `${CLASSNAME}-navbutton`;
const CLASSNAME_NAV = `${CLASSNAME}-nav`;
const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`;
Expand All @@ -82,6 +91,108 @@ const MIN_ZOOM = 0.1;
const SCROLL_ZOOM_TIMEOUT = 250;
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 {
images: ILightboxImage[];
isVisible: boolean;
Expand Down Expand Up @@ -117,7 +228,7 @@ export const LightboxComponent: React.FC<IProps> = ({
const [index, setIndex] = useState<number | null>(null);
const [movingLeft, setMovingLeft] = useState(false);
const oldIndex = useRef<number | null>(null);
const [instantTransition, setInstantTransition] = useState(false);
const [transition, setTransition] = useState<string | null>(null);
const [isSwitchingPage, setIsSwitchingPage] = useState(true);
const [isFullscreen, setFullscreen] = useState(false);
const [showOptions, setShowOptions] = useState(false);
Expand All @@ -144,7 +255,6 @@ export const LightboxComponent: React.FC<IProps> = ({

const containerRef = useRef<HTMLDivElement | null>(null);
const overlayTarget = useRef<HTMLButtonElement | null>(null);
const carouselRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null);
const navRef = useRef<HTMLDivElement | null>(null);
const clearIntervalCallback = useRef<() => void>();
Expand Down Expand Up @@ -230,15 +340,15 @@ export const LightboxComponent: React.FC<IProps> = ({
}
}, [isSwitchingPage, images, index]);

const disableInstantTransition = useDebounce(
() => setInstantTransition(false),
400
);
const restoreTransition = useDebounce(() => setTransition(null), 400);

const setInstant = useCallback(() => {
setInstantTransition(true);
disableInstantTransition();
}, [disableInstantTransition]);
const overrideTransition = useCallback(
(t: string) => {
setTransition(t);
restoreTransition();
},
[restoreTransition]
);

useEffect(() => {
if (images.length < 2) return;
Expand Down Expand Up @@ -402,12 +512,12 @@ export const LightboxComponent: React.FC<IProps> = ({
const handleKey = useCallback(
(e: KeyboardEvent) => {
if (e.repeat && (e.key === "ArrowRight" || e.key === "ArrowLeft"))
setInstant();
overrideTransition(CLASSNAME_INSTANT);
if (e.key === "ArrowLeft") handleLeft();
else if (e.key === "ArrowRight") handleRight();
else if (e.key === "Escape") close();
},
[setInstant, handleLeft, handleRight, close]
[overrideTransition, handleLeft, handleRight, close]
);
const handleFullScreenChange = () => {
if (clearIntervalCallback.current) {
Expand Down Expand Up @@ -848,45 +958,24 @@ export const LightboxComponent: React.FC<IProps> = ({
<Icon icon={faChevronLeft} />
</Button>
)}

<div
className={cx(CLASSNAME_CAROUSEL, {
[CLASSNAME_INSTANT]: instantTransition,
})}
style={{ left: `${currentIndex * -100}vw` }}
ref={carouselRef}
>
{images.map((image, i) => (
<div className={`${CLASSNAME_IMAGE}`} 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] ?? {})}
/>
) : undefined}
</div>
))}
</div>

<LightboxCarousel
transition={transition}
currentIndex={currentIndex}
images={images}
displayMode={displayMode}
lightboxSettings={lightboxSettings}
resetPosition={resetPosition}
zoom={zoom}
scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
firstScroll={firstScroll}
inScrollGroup={inScrollGroup}
movingLeft={movingLeft}
updateZoom={updateZoom}
debouncedScrollReset={debouncedScrollReset}
handleLeft={handleLeft}
handleRight={handleRight}
overrideTransition={overrideTransition}
/>
{allowNavigation && (
<Button
variant="link"
Expand Down
Loading