mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Lightbox infinite scrolling improvements (#3894)
This commit is contained in:
@@ -73,6 +73,9 @@ const CLASSNAME_NAVSELECTED = `${CLASSNAME_NAV}-selected`;
|
|||||||
const DEFAULT_SLIDESHOW_DELAY = 5000;
|
const DEFAULT_SLIDESHOW_DELAY = 5000;
|
||||||
const SECONDS_TO_MS = 1000;
|
const SECONDS_TO_MS = 1000;
|
||||||
const MIN_VALID_INTERVAL_SECONDS = 1;
|
const MIN_VALID_INTERVAL_SECONDS = 1;
|
||||||
|
const MIN_ZOOM = 0.1;
|
||||||
|
const SCROLL_ZOOM_TIMEOUT = 250;
|
||||||
|
const ZOOM_NONE_EPSILON = 0.015;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
images: ILightboxImage[];
|
images: ILightboxImage[];
|
||||||
@@ -120,6 +123,18 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
const oldImages = useRef<ILightboxImage[]>([]);
|
const oldImages = useRef<ILightboxImage[]>([]);
|
||||||
|
|
||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
|
|
||||||
|
function updateZoom(v: number) {
|
||||||
|
if (v < MIN_ZOOM) {
|
||||||
|
setZoom(MIN_ZOOM);
|
||||||
|
} else if (Math.abs(v - 1) < ZOOM_NONE_EPSILON) {
|
||||||
|
// "snap to 1" effect: if new zoom is close to 1, set to 1
|
||||||
|
setZoom(1);
|
||||||
|
} else {
|
||||||
|
setZoom(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [resetPosition, setResetPosition] = useState(false);
|
const [resetPosition, setResetPosition] = useState(false);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -373,6 +388,14 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const firstScroll = useRef<number | null>(null);
|
||||||
|
const inScrollGroup = useRef(false);
|
||||||
|
|
||||||
|
const debouncedScrollReset = useDebounce(() => {
|
||||||
|
firstScroll.current = null;
|
||||||
|
inScrollGroup.current = false;
|
||||||
|
}, SCROLL_ZOOM_TIMEOUT);
|
||||||
|
|
||||||
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"))
|
||||||
@@ -842,14 +865,17 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
lightboxSettings?.scrollMode ??
|
lightboxSettings?.scrollMode ??
|
||||||
GQL.ImageLightboxScrollMode.Zoom
|
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}
|
onLeft={handleLeft}
|
||||||
onRight={handleRight}
|
onRight={handleRight}
|
||||||
alignBottom={movingLeft}
|
|
||||||
zoom={i === currentIndex ? zoom : 1}
|
|
||||||
current={i === currentIndex}
|
|
||||||
scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
|
|
||||||
setZoom={(v) => setZoom(v)}
|
|
||||||
resetPosition={resetPosition}
|
|
||||||
isVideo={isVideo(image.visual_files?.[0] ?? {})}
|
isVideo={isVideo(image.visual_files?.[0] ?? {})}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import React, { useEffect, useRef, useState, useCallback } from "react";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
const ZOOM_STEP = 1.1;
|
const ZOOM_STEP = 1.1;
|
||||||
|
const ZOOM_FACTOR = 700;
|
||||||
|
const SCROLL_GROUP_THRESHOLD = 8;
|
||||||
|
const SCROLL_GROUP_EXIT_THRESHOLD = 4;
|
||||||
|
const SCROLL_INFINITE_THRESHOLD = 10;
|
||||||
const SCROLL_PAN_STEP = 75;
|
const SCROLL_PAN_STEP = 75;
|
||||||
|
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`;
|
||||||
@@ -53,10 +58,15 @@ interface IProps {
|
|||||||
resetPosition?: boolean;
|
resetPosition?: boolean;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
scrollAttemptsBeforeChange: number;
|
scrollAttemptsBeforeChange: number;
|
||||||
|
// these refs must be outside of LightboxImage,
|
||||||
|
// since they need to be shared between all LightboxImages
|
||||||
|
firstScroll: React.MutableRefObject<number | null>;
|
||||||
|
inScrollGroup: React.MutableRefObject<boolean>;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
// set to true to align image with bottom instead of top
|
// set to true to align image with bottom instead of top
|
||||||
alignBottom?: boolean;
|
alignBottom?: boolean;
|
||||||
setZoom: (v: number) => void;
|
setZoom: (v: number) => void;
|
||||||
|
debouncedScrollReset: () => void;
|
||||||
onLeft: () => void;
|
onLeft: () => void;
|
||||||
onRight: () => void;
|
onRight: () => void;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
@@ -64,17 +74,20 @@ interface IProps {
|
|||||||
|
|
||||||
export const LightboxImage: React.FC<IProps> = ({
|
export const LightboxImage: React.FC<IProps> = ({
|
||||||
src,
|
src,
|
||||||
onLeft,
|
|
||||||
onRight,
|
|
||||||
displayMode,
|
displayMode,
|
||||||
scaleUp,
|
scaleUp,
|
||||||
scrollMode,
|
scrollMode,
|
||||||
alignBottom,
|
resetPosition,
|
||||||
zoom,
|
zoom,
|
||||||
scrollAttemptsBeforeChange,
|
scrollAttemptsBeforeChange,
|
||||||
|
firstScroll,
|
||||||
|
inScrollGroup,
|
||||||
current,
|
current,
|
||||||
|
alignBottom,
|
||||||
setZoom,
|
setZoom,
|
||||||
resetPosition,
|
debouncedScrollReset,
|
||||||
|
onLeft,
|
||||||
|
onRight,
|
||||||
isVideo,
|
isVideo,
|
||||||
}) => {
|
}) => {
|
||||||
const [defaultZoom, setDefaultZoom] = useState(1);
|
const [defaultZoom, setDefaultZoom] = useState(1);
|
||||||
@@ -253,12 +266,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
calculateInitialPosition,
|
calculateInitialPosition,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getScrollMode(
|
function getScrollMode(ev: React.WheelEvent) {
|
||||||
ev:
|
|
||||||
| React.WheelEvent<HTMLImageElement>
|
|
||||||
| React.WheelEvent<HTMLVideoElement>
|
|
||||||
| React.WheelEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
switch (scrollMode) {
|
switch (scrollMode) {
|
||||||
case GQL.ImageLightboxScrollMode.Zoom:
|
case GQL.ImageLightboxScrollMode.Zoom:
|
||||||
@@ -271,54 +279,83 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
return scrollMode;
|
return scrollMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContainerScroll(
|
function onContainerScroll(ev: React.WheelEvent) {
|
||||||
ev:
|
|
||||||
| React.WheelEvent<HTMLImageElement>
|
|
||||||
| React.WheelEvent<HTMLVideoElement>
|
|
||||||
| React.WheelEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
// don't zoom if mouse isn't over image
|
// don't zoom if mouse isn't over image
|
||||||
if (getScrollMode(ev) === GQL.ImageLightboxScrollMode.PanY) {
|
if (getScrollMode(ev) === GQL.ImageLightboxScrollMode.PanY) {
|
||||||
onImageScroll(ev);
|
onImageScroll(ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageScrollPanY(
|
function onLeftScroll(
|
||||||
ev:
|
ev: React.WheelEvent,
|
||||||
| React.WheelEvent<HTMLImageElement>
|
scrollable: boolean,
|
||||||
| React.WheelEvent<HTMLVideoElement>
|
infinite: boolean
|
||||||
| React.WheelEvent<HTMLDivElement>
|
|
||||||
) {
|
) {
|
||||||
if (current) {
|
if (infinite) {
|
||||||
const [minY, maxY] = minMaxY(zoom * defaultZoom);
|
// for infinite scrolls, only change once per scroll "group"
|
||||||
|
if (ev.deltaY <= -SCROLL_GROUP_THRESHOLD) {
|
||||||
const scrollable = positionY !== maxY || positionY !== minY;
|
if (!inScrollGroup.current) {
|
||||||
|
onLeft();
|
||||||
let newPositionY =
|
}
|
||||||
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
}
|
||||||
|
} else {
|
||||||
// #2389 - if scroll up and at top, then go to previous image
|
|
||||||
// if scroll down and at bottom, then go to next image
|
|
||||||
if (newPositionY > maxY && positionY === maxY) {
|
|
||||||
// #2535 - require additional scrolls before changing page
|
// #2535 - require additional scrolls before changing page
|
||||||
if (
|
if (
|
||||||
!scrollable ||
|
!scrollable ||
|
||||||
scrollAttempts.current <= -scrollAttemptsBeforeChange
|
scrollAttempts.current <= -scrollAttemptsBeforeChange
|
||||||
) {
|
) {
|
||||||
|
scrollAttempts.current = 0;
|
||||||
onLeft();
|
onLeft();
|
||||||
} else {
|
} else {
|
||||||
scrollAttempts.current--;
|
scrollAttempts.current--;
|
||||||
}
|
}
|
||||||
} else if (newPositionY < minY && positionY === minY) {
|
}
|
||||||
// #2535 - require additional scrolls before changing page
|
}
|
||||||
if (
|
|
||||||
!scrollable ||
|
function onRightScroll(
|
||||||
scrollAttempts.current >= scrollAttemptsBeforeChange
|
ev: React.WheelEvent,
|
||||||
|
scrollable: boolean,
|
||||||
|
infinite: boolean
|
||||||
) {
|
) {
|
||||||
|
if (infinite) {
|
||||||
|
// for infinite scrolls, only change once per scroll "group"
|
||||||
|
if (ev.deltaY >= SCROLL_GROUP_THRESHOLD) {
|
||||||
|
if (!inScrollGroup.current) {
|
||||||
|
onRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// #2535 - require additional scrolls before changing page
|
||||||
|
if (!scrollable || scrollAttempts.current >= scrollAttemptsBeforeChange) {
|
||||||
|
scrollAttempts.current = 0;
|
||||||
onRight();
|
onRight();
|
||||||
} else {
|
} else {
|
||||||
scrollAttempts.current++;
|
scrollAttempts.current++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onImageScrollPanY(ev: React.WheelEvent, infinite: boolean) {
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const [minY, maxY] = minMaxY(zoom * defaultZoom);
|
||||||
|
|
||||||
|
const scrollable = positionY !== maxY || positionY !== minY;
|
||||||
|
|
||||||
|
let newPositionY: number;
|
||||||
|
if (infinite) {
|
||||||
|
newPositionY = positionY - ev.deltaY / SCROLL_PAN_FACTOR;
|
||||||
|
} else {
|
||||||
|
newPositionY =
|
||||||
|
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #2389 - if scroll up and at top, then go to previous image
|
||||||
|
// if scroll down and at bottom, then go to next image
|
||||||
|
if (newPositionY > maxY && positionY === maxY) {
|
||||||
|
onLeftScroll(ev, scrollable, infinite);
|
||||||
|
} else if (newPositionY < minY && positionY === minY) {
|
||||||
|
onRightScroll(ev, scrollable, infinite);
|
||||||
} else {
|
} else {
|
||||||
scrollAttempts.current = 0;
|
scrollAttempts.current = 0;
|
||||||
|
|
||||||
@@ -331,31 +368,45 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function onImageScroll(
|
function onImageScroll(ev: React.WheelEvent) {
|
||||||
ev:
|
const absDeltaY = Math.abs(ev.deltaY);
|
||||||
| React.WheelEvent<HTMLImageElement>
|
const firstDeltaY = firstScroll.current;
|
||||||
| React.WheelEvent<HTMLVideoElement>
|
// detect infinite scrolling (mousepad, mouse with infinite scrollwheel)
|
||||||
| React.WheelEvent<HTMLDivElement>
|
const infinite =
|
||||||
) {
|
// scrolling is infinite if deltaY is small
|
||||||
const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
|
absDeltaY < SCROLL_INFINITE_THRESHOLD ||
|
||||||
|
// or if scroll events come quickly and the first one was small
|
||||||
|
(firstDeltaY !== null &&
|
||||||
|
Math.abs(firstDeltaY) < SCROLL_INFINITE_THRESHOLD);
|
||||||
|
|
||||||
switch (getScrollMode(ev)) {
|
switch (getScrollMode(ev)) {
|
||||||
case GQL.ImageLightboxScrollMode.Zoom:
|
case GQL.ImageLightboxScrollMode.Zoom:
|
||||||
|
let percent: number;
|
||||||
|
if (infinite) {
|
||||||
|
percent = 1 - ev.deltaY / ZOOM_FACTOR;
|
||||||
|
} else {
|
||||||
|
percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
|
||||||
|
}
|
||||||
setZoom(zoom * percent);
|
setZoom(zoom * percent);
|
||||||
break;
|
break;
|
||||||
case GQL.ImageLightboxScrollMode.PanY:
|
case GQL.ImageLightboxScrollMode.PanY:
|
||||||
onImageScrollPanY(ev);
|
onImageScrollPanY(ev, infinite);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (firstDeltaY === null) {
|
||||||
|
firstScroll.current = ev.deltaY;
|
||||||
|
}
|
||||||
|
if (absDeltaY >= SCROLL_GROUP_THRESHOLD) {
|
||||||
|
inScrollGroup.current = true;
|
||||||
|
} else if (absDeltaY <= SCROLL_GROUP_EXIT_THRESHOLD) {
|
||||||
|
// only "exit" the scroll group if speed has slowed considerably
|
||||||
|
inScrollGroup.current = false;
|
||||||
|
}
|
||||||
|
debouncedScrollReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageMouseOver(
|
function onImageMouseOver(ev: React.MouseEvent) {
|
||||||
ev:
|
|
||||||
| React.MouseEvent<HTMLImageElement, MouseEvent>
|
|
||||||
| React.MouseEvent<HTMLVideoElement, MouseEvent>
|
|
||||||
) {
|
|
||||||
if (!moving) return;
|
if (!moving) return;
|
||||||
|
|
||||||
if (!ev.buttons) {
|
if (!ev.buttons) {
|
||||||
@@ -371,22 +422,14 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
setPositionY(positionY + posY);
|
setPositionY(positionY + posY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageMouseDown(
|
function onImageMouseDown(ev: React.MouseEvent) {
|
||||||
ev:
|
|
||||||
| React.MouseEvent<HTMLImageElement, MouseEvent>
|
|
||||||
| React.MouseEvent<HTMLVideoElement, MouseEvent>
|
|
||||||
) {
|
|
||||||
startPoints.current = [ev.pageX, ev.pageY];
|
startPoints.current = [ev.pageX, ev.pageY];
|
||||||
setMoving(true);
|
setMoving(true);
|
||||||
|
|
||||||
mouseDownEvent.current = ev.nativeEvent;
|
mouseDownEvent.current = ev.nativeEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageMouseUp(
|
function onImageMouseUp(ev: React.MouseEvent) {
|
||||||
ev:
|
|
||||||
| React.MouseEvent<HTMLImageElement, MouseEvent>
|
|
||||||
| React.MouseEvent<HTMLVideoElement, MouseEvent>
|
|
||||||
) {
|
|
||||||
if (ev.button !== 0) return;
|
if (ev.button !== 0) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -412,12 +455,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchStart(
|
function onTouchStart(ev: React.TouchEvent) {
|
||||||
ev:
|
|
||||||
| React.TouchEvent<HTMLImageElement>
|
|
||||||
| React.TouchEvent<HTMLVideoElement>
|
|
||||||
| React.TouchEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (ev.touches.length === 1) {
|
if (ev.touches.length === 1) {
|
||||||
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
|
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
|
||||||
@@ -425,12 +463,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchMove(
|
function onTouchMove(ev: React.TouchEvent) {
|
||||||
ev:
|
|
||||||
| React.TouchEvent<HTMLImageElement>
|
|
||||||
| React.TouchEvent<HTMLVideoElement>
|
|
||||||
| React.TouchEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
if (!moving) return;
|
if (!moving) return;
|
||||||
|
|
||||||
if (ev.touches.length === 1) {
|
if (ev.touches.length === 1) {
|
||||||
@@ -443,12 +476,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerDown(
|
function onPointerDown(ev: React.PointerEvent) {
|
||||||
ev:
|
|
||||||
| React.PointerEvent<HTMLImageElement>
|
|
||||||
| React.PointerEvent<HTMLVideoElement>
|
|
||||||
| React.PointerEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
// replace pointer event with the same id, if applicable
|
// replace pointer event with the same id, if applicable
|
||||||
pointerCache.current = pointerCache.current.filter(
|
pointerCache.current = pointerCache.current.filter(
|
||||||
(e) => e.pointerId !== ev.pointerId
|
(e) => e.pointerId !== ev.pointerId
|
||||||
@@ -458,12 +486,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
prevDiff.current = undefined;
|
prevDiff.current = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerUp(
|
function onPointerUp(ev: React.PointerEvent) {
|
||||||
ev:
|
|
||||||
| React.PointerEvent<HTMLImageElement>
|
|
||||||
| React.PointerEvent<HTMLVideoElement>
|
|
||||||
| React.PointerEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
for (let i = 0; i < pointerCache.current.length; i++) {
|
for (let i = 0; i < pointerCache.current.length; i++) {
|
||||||
if (pointerCache.current[i].pointerId === ev.pointerId) {
|
if (pointerCache.current[i].pointerId === ev.pointerId) {
|
||||||
pointerCache.current.splice(i, 1);
|
pointerCache.current.splice(i, 1);
|
||||||
@@ -472,12 +495,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerMove(
|
function onPointerMove(ev: React.PointerEvent) {
|
||||||
ev:
|
|
||||||
| React.PointerEvent<HTMLImageElement>
|
|
||||||
| React.PointerEvent<HTMLVideoElement>
|
|
||||||
| React.PointerEvent<HTMLDivElement>
|
|
||||||
) {
|
|
||||||
// find the event in the cache
|
// find the event in the cache
|
||||||
const cachedIndex = pointerCache.current.findIndex(
|
const cachedIndex = pointerCache.current.findIndex(
|
||||||
(c) => c.pointerId === ev.pointerId
|
(c) => c.pointerId === ev.pointerId
|
||||||
@@ -543,14 +561,14 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
style={customStyle}
|
style={customStyle}
|
||||||
onWheel={current ? (e) => onImageScroll(e) : undefined}
|
onWheel={current ? (e) => onImageScroll(e) : undefined}
|
||||||
onMouseDown={(e) => onImageMouseDown(e)}
|
onMouseDown={onImageMouseDown}
|
||||||
onMouseUp={(e) => onImageMouseUp(e)}
|
onMouseUp={onImageMouseUp}
|
||||||
onMouseMove={(e) => onImageMouseOver(e)}
|
onMouseMove={onImageMouseOver}
|
||||||
onTouchStart={(e) => onTouchStart(e)}
|
onTouchStart={onTouchStart}
|
||||||
onTouchMove={(e) => onTouchMove(e)}
|
onTouchMove={onTouchMove}
|
||||||
onPointerDown={(e) => onPointerDown(e)}
|
onPointerDown={onPointerDown}
|
||||||
onPointerUp={(e) => onPointerUp(e)}
|
onPointerUp={onPointerUp}
|
||||||
onPointerMove={(e) => onPointerMove(e)}
|
onPointerMove={onPointerMove}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|||||||
Reference in New Issue
Block a user