Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

610 improve navigation of timeline #637

Draft
wants to merge 12 commits into
base: staging
Choose a base branch
from
162 changes: 162 additions & 0 deletions projects/bp-gallery/src/components/common/ScrollNavigationArrows.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [isPressedRight, setPressedRight] = useState<boolean>(false);
const disallowClickLeft = useRef<boolean>(false);
const disallowClickRight = useRef<boolean>(false);

const leftPressTimeout = useRef<NodeJS.Timeout>();
const rightPressTimeout = useRef<NodeJS.Timeout>();

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 (
<div>
<div
className={`absolute top-0 left-0 h-full flex ${isVisibleLeft ? 'visible' : 'hidden'}`}
>
<IconButton
className='!bg-[#7e241d] !text-white !my-auto !shadow-xl !ml-1'
onClick={e => {
onPressLeft(e);
}}
onMouseDown={e => {
setPressedLeft(true);
onPressLeft(e);
}}
onTouchStart={e => {
setPressedLeft(true);
onPressLeft(e);
}}
onMouseUp={() => {
setPressedLeft(false);
}}
onMouseLeave={() => {
setPressedLeft(false);
}}
onTouchEnd={() => {
setPressedLeft(false);
}}
>
<FastRewind className='icon' />
</IconButton>
</div>
<div
className={`absolute top-0 right-0 h-full flex ${isVisibleRight ? 'visible' : 'hidden'}`}
>
<IconButton
className='!bg-[#7e241d] !text-white !my-auto !shadow-xl !mr-1'
onClick={e => {
onPressRight(e);
}}
onMouseDown={e => {
setPressedRight(true);
onPressRight(e);
}}
onTouchStart={e => {
setPressedRight(true);
onPressRight(e);
}}
onMouseUp={() => {
setPressedRight(false);
}}
onMouseLeave={() => {
setPressedRight(false);
}}
onTouchEnd={() => {
setPressedRight(false);
}}
>
<FastForward className='icon' />
</IconButton>
</div>
</div>
);
} else {
return <></>;
}
};

export default ScrollNavigationArrows;
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -109,6 +110,8 @@ const HorizontalPictureGrid = ({

const [focusedPicture, setFocusedPicture] = useState<string | undefined>(undefined);
const [transitioning, setTransitioning] = useState<boolean>(false);
const [isVisibleLeft, setVisibleLeft] = useState<boolean>(true);
const [isVisibleRight, setVisibleRight] = useState<boolean>(true);

const selectedPicture = useRef<FlatPicture | undefined>();

Expand Down Expand Up @@ -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();
Expand All @@ -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(() => {
Expand All @@ -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
Expand Down Expand Up @@ -349,6 +370,33 @@ const HorizontalPictureGrid = ({
>
{content}
</ScrollBar>
<ScrollNavigationArrows
onClickLeft={() => {
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}
/>
</div>
{focusedPicture && !transitioning && (
<Portal container={root}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -34,6 +35,10 @@ const PictureTimeline = ({
0.06
);

const [drag, setDrag] = useState<boolean>(false);
const [dragged, setDragged] = useState<boolean>(false);
const lastPos = useRef<number | undefined>();

useEffect(() => {
if (scrollBarRef.current) {
scrollBarRef.current.scrollLeft = scrollLeft;
Expand Down Expand Up @@ -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 ${
Expand All @@ -89,16 +98,58 @@ const PictureTimeline = ({
const updateOnScrollX = useMemo(() => debounce(updateDate, 500), [updateDate]);

return (
<div>
<div
onMouseDown={e => {
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;
}
}}
>
<div className='flex'>
<ArrowDropDown className='mx-auto scale-[1.75]' />
</div>
<div className='relative'>
<div className='overflow-x-scroll' ref={scrollBarRef} onScroll={updateOnScrollX}>
<div className='flex'>
<ul className='py-[16px] px-[50%] mt-0 whitespace-nowrap'>{listItems}</ul>
<div className='relative mb-2'>
<div className='overflow-x-scroll z-0' ref={scrollBarRef} onScroll={updateOnScrollX}>
<div className='flex pb-2'>
<ul className='py-[16px] px-[50%] my-0 whitespace-nowrap'>{listItems}</ul>
</div>
</div>
<ScrollNavigationArrows
onClickLeft={() => {
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}
/>
</div>
</div>
);
Expand Down
Loading