diff --git a/projects/bp-gallery/src/components/common/ScrollNavigationArrows.tsx b/projects/bp-gallery/src/components/common/ScrollNavigationArrows.tsx new file mode 100644 index 000000000..f4ae01c74 --- /dev/null +++ b/projects/bp-gallery/src/components/common/ScrollNavigationArrows.tsx @@ -0,0 +1,162 @@ +import { FastForward, FastRewind } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { MobileContext } from '../provider/MobileProvider'; + +const ScrollNavigationArrows = ({ + onClickLeft, + onClickRight, + onLongPressLeft, + onLongPressRight, + longPressTimeoutLeft = 1000, + longPressTimeoutRight = 1000, + isVisibleLeft = true, + isVisibleRight = true, + allowLongPressLeft = true, + allowLongPressRight = true, + showOnMobile = true, +}: { + onClickLeft: () => void; + onClickRight: () => void; + onLongPressLeft?: () => void; + onLongPressRight?: () => void; + longPressTimeoutLeft?: number; + longPressTimeoutRight?: number; + isVisibleLeft?: boolean; + isVisibleRight?: boolean; + allowLongPressLeft?: boolean; + allowLongPressRight?: boolean; + showOnMobile?: boolean; +}) => { + const { isMobile } = useContext(MobileContext); + const [isPressedLeft, setPressedLeft] = useState(false); + const [isPressedRight, setPressedRight] = useState(false); + const disallowClickLeft = useRef(false); + const disallowClickRight = useRef(false); + + const leftPressTimeout = useRef(); + const rightPressTimeout = useRef(); + + const onPressLeft = (e: any) => { + if (e?.type === 'click') { + if (!disallowClickLeft.current) { + onClickLeft(); + } else { + disallowClickLeft.current = false; + } + } else { + if (!e) { + // e is undefined if this function is called via timeout and therefore is accessed via a long press instead of a click + disallowClickLeft.current = true; + } + if (disallowClickLeft.current) { + onLongPressLeft ? onLongPressLeft() : onClickLeft(); + } + if (allowLongPressLeft) { + leftPressTimeout.current = setTimeout(onPressLeft, longPressTimeoutLeft); + } + } + }; + + const onPressRight = (e: any) => { + if (e?.type === 'click') { + if (!disallowClickRight.current) { + onClickRight(); + } else { + disallowClickRight.current = false; + } + } else { + if (!e) { + // e is undefined if this function is called via timeout and therefore is accessed via a long press instead of a click + disallowClickRight.current = true; + } + if (disallowClickRight.current) { + onLongPressRight ? onLongPressRight() : onClickRight(); + } + if (allowLongPressRight) { + rightPressTimeout.current = setTimeout(onPressRight, longPressTimeoutRight); + } + } + }; + + useEffect(() => { + if (!isPressedLeft) { + clearTimeout(leftPressTimeout.current); + } + }, [isPressedLeft]); + + useEffect(() => { + if (!isPressedRight) { + clearTimeout(rightPressTimeout.current); + } + }, [isPressedRight]); + + if (!isMobile || showOnMobile) { + return ( +
+
+ { + onPressLeft(e); + }} + onMouseDown={e => { + setPressedLeft(true); + onPressLeft(e); + }} + onTouchStart={e => { + setPressedLeft(true); + onPressLeft(e); + }} + onMouseUp={() => { + setPressedLeft(false); + }} + onMouseLeave={() => { + setPressedLeft(false); + }} + onTouchEnd={() => { + setPressedLeft(false); + }} + > + + +
+
+ { + onPressRight(e); + }} + onMouseDown={e => { + setPressedRight(true); + onPressRight(e); + }} + onTouchStart={e => { + setPressedRight(true); + onPressRight(e); + }} + onMouseUp={() => { + setPressedRight(false); + }} + onMouseLeave={() => { + setPressedRight(false); + }} + onTouchEnd={() => { + setPressedRight(false); + }} + > + + +
+
+ ); + } else { + return <>; + } +}; + +export default ScrollNavigationArrows; diff --git a/projects/bp-gallery/src/components/common/picture-gallery/HorizontalPictureGrid.tsx b/projects/bp-gallery/src/components/common/picture-gallery/HorizontalPictureGrid.tsx index f19ec9583..b34be68c0 100644 --- a/projects/bp-gallery/src/components/common/picture-gallery/HorizontalPictureGrid.tsx +++ b/projects/bp-gallery/src/components/common/picture-gallery/HorizontalPictureGrid.tsx @@ -7,6 +7,7 @@ import { pushHistoryWithoutRouter } from '../../../helpers/history'; import useGetPictures, { TextFilter } from '../../../hooks/get-pictures.hook'; import { FlatPicture, PictureOverviewType } from '../../../types/additionalFlatTypes'; import PictureView from '../../views/picture/PictureView'; +import ScrollNavigationArrows from '../ScrollNavigationArrows'; import PicturePreview from './PicturePreview'; import { zoomIntoPicture, zoomOutOfPicture } from './helpers/picture-animations'; @@ -109,6 +110,8 @@ const HorizontalPictureGrid = ({ const [focusedPicture, setFocusedPicture] = useState(undefined); const [transitioning, setTransitioning] = useState(false); + const [isVisibleLeft, setVisibleLeft] = useState(true); + const [isVisibleRight, setVisibleRight] = useState(true); const selectedPicture = useRef(); @@ -249,7 +252,10 @@ const HorizontalPictureGrid = ({ const updateCurrentValue = useCallback(() => { const field = Math.ceil((scrollBarRef.current?.scrollLeft ?? 0) / IMAGE_WIDGET_WIDTH); - const index = field * IMAGES_PER_WIDGET + ((leftPictures?.length ?? 0) % IMAGES_PER_WIDGET); + const index = Math.max( + 0, + (field - 1) * IMAGES_PER_WIDGET + ((leftPictures?.length ?? 0) % IMAGES_PER_WIDGET) + ); selectedPicture.current = pictures.length > index && index >= 0 ? pictures[index] : pictures[pictures.length - 1]; const year = new Date(selectedPicture.current.time_range_tag?.start as Date).getFullYear(); @@ -270,6 +276,13 @@ const HorizontalPictureGrid = ({ } else if (!allowDateUpdate.current) { allowDateUpdate.current = true; } + + if (!scrollBarRef.current) return; + setVisibleLeft(scrollBarRef.current.scrollLeft > 0); + setVisibleRight( + scrollBarRef.current.scrollLeft < + scrollBarRef.current.scrollWidth - scrollBarRef.current.clientWidth + ); }, [leftPictures?.length, leftResult.loading, pictures, rightResult.loading, setDate]); useEffect(() => { @@ -287,9 +300,17 @@ const HorizontalPictureGrid = ({ pictureLength.current = leftPictures?.length ?? 0; lastScrollPos.current = Math.max(newWidgetCount - oldWidgetCount, 1) * IMAGE_WIDGET_WIDTH; scrollBarRef.current.scrollLeft = - Math.max(newWidgetCount - oldWidgetCount, 1) * IMAGE_WIDGET_WIDTH; + Math.max(newWidgetCount - oldWidgetCount, 0) * IMAGE_WIDGET_WIDTH; }, [leftPictures, leftResult.loading]); + useEffect(() => { + if (!scrollBarRef.current) return; + setVisibleRight( + scrollBarRef.current.scrollLeft < + scrollBarRef.current.scrollWidth - scrollBarRef.current.clientWidth - 5 //offset + ); + }, [rightResult.loading]); + useEffect(() => { if (leftResult.loading || rightResult.loading) return; const lowerBorder = pictures.length @@ -349,6 +370,33 @@ const HorizontalPictureGrid = ({ > {content} + { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft -= IMAGE_WIDGET_WIDTH; + } + }} + onClickRight={() => { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft += IMAGE_WIDGET_WIDTH; + } + }} + onLongPressLeft={() => { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft -= 100; + } + }} + onLongPressRight={() => { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft += 100; + } + }} + longPressTimeoutLeft={200} + longPressTimeoutRight={200} + isVisibleLeft={isVisibleLeft} + isVisibleRight={isVisibleRight} + showOnMobile={false} + /> {focusedPicture && !transitioning && ( diff --git a/projects/bp-gallery/src/components/common/picture-gallery/PictureTimeline.tsx b/projects/bp-gallery/src/components/common/picture-gallery/PictureTimeline.tsx index d38b93361..f22adf3c6 100644 --- a/projects/bp-gallery/src/components/common/picture-gallery/PictureTimeline.tsx +++ b/projects/bp-gallery/src/components/common/picture-gallery/PictureTimeline.tsx @@ -1,7 +1,8 @@ import { ArrowDropDown } from '@mui/icons-material'; import { debounce } from 'lodash'; -import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react'; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAnimate } from '../../../hooks/animate.hook'; +import ScrollNavigationArrows from '../ScrollNavigationArrows'; // for future work export const enum TimeStepType { @@ -34,6 +35,10 @@ const PictureTimeline = ({ 0.06 ); + const [drag, setDrag] = useState(false); + const [dragged, setDragged] = useState(false); + const lastPos = useRef(); + useEffect(() => { if (scrollBarRef.current) { scrollBarRef.current.scrollLeft = scrollLeft; @@ -62,7 +67,11 @@ const PictureTimeline = ({ key={year} className='inline cursor-pointer' onClick={() => { - setDate(year); + if (dragged) { + setDragged(false); + } else { + setDate(year); + } }} style={{ padding: `40px ${ @@ -89,16 +98,58 @@ const PictureTimeline = ({ const updateOnScrollX = useMemo(() => debounce(updateDate, 500), [updateDate]); return ( -
+
{ + setDrag(true); + lastPos.current = e.nativeEvent.offsetX; + }} + onMouseUp={e => { + setDrag(false); + lastPos.current = undefined; + }} + onMouseMove={e => { + if ( + scrollBarRef.current && + drag && + lastPos.current && + e.nativeEvent.offsetY > 0 && + e.nativeEvent.offsetY <= 80 + ) { + setDragged(true); + scrollBarRef.current.scrollLeft -= e.nativeEvent.offsetX - lastPos.current; + lastPos.current = e.nativeEvent.offsetX; + } else { + setDrag(false); + lastPos.current = undefined; + } + }} + >
-
-
-
-
    {listItems}
+
+
+
+
    {listItems}
+ { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft -= singleElementWidth; + } + }} + onClickRight={() => { + if (scrollBarRef.current) { + scrollBarRef.current.scrollLeft += singleElementWidth; + } + }} + longPressTimeoutLeft={250} + longPressTimeoutRight={250} + isVisibleLeft={date > start} + isVisibleRight={date < end} + showOnMobile={false} + />
);