@@ -103,7 +103,11 @@ interface IProps {
103103 onLeft : ( ) => void ;
104104 onRight : ( ) => void ;
105105 moveCarousel : ( v : number ) => void ;
106- releaseCarousel : ( ev : React . TouchEvent , swipeDuration : number ) => void ;
106+ releaseCarousel : (
107+ ev : React . PointerEvent ,
108+ swipeDuration : number ,
109+ cancelled : boolean
110+ ) => void ;
107111 isVideo : boolean ;
108112}
109113
@@ -130,7 +134,6 @@ export const LightboxImage: React.FC<IProps> = ({
130134 isVideo,
131135} ) => {
132136 const [ defaultZoom , setDefaultZoom ] = useState < number | null > ( null ) ;
133- const [ moving , setMoving ] = useState ( false ) ;
134137 const [ positionX , setPositionX ] = useState ( 0 ) ;
135138 const [ positionY , setPositionY ] = useState ( 0 ) ;
136139 const [ imageWidth , setImageWidth ] = useState ( width ) ;
@@ -139,10 +142,12 @@ export const LightboxImage: React.FC<IProps> = ({
139142 const [ containerRef , { width : boxWidth , height : boxHeight } ] =
140143 useContainerDimensions ( ) ;
141144
142- const mouseDownEvent = useRef < MouseEvent > ( ) ;
143145 const resetPositionRef = useRef ( resetPosition ) ;
144146
145- const startPoints = useRef < number [ ] > ( [ 0 , 0 ] ) ;
147+ // Panning and swipe navigation are tracked in startPoint. Pinch zoom is
148+ // tracked in prevDiff. They are undefined if no action of that type is in
149+ // progress.
150+ const startPoint = useRef < number [ ] | undefined > ( ) ;
146151 const startTime = useRef < number > ( 0 ) ;
147152 const pointerCache = useRef < React . PointerEvent [ ] > ( [ ] ) ;
148153 const prevDiff = useRef < number | undefined > ( ) ;
@@ -450,135 +455,81 @@ export const LightboxImage: React.FC<IProps> = ({
450455 debouncedScrollReset ( ) ;
451456 }
452457
453- function onImageMouseOver ( ev : React . MouseEvent ) {
454- if ( ! moving ) return ;
455-
456- if ( ! ev . buttons ) {
457- setMoving ( false ) ;
458- return ;
459- }
460-
461- const deltaX = ev . pageX - startPoints . current [ 0 ] ;
462- const deltaY = ev . pageY - startPoints . current [ 1 ] ;
463- startPoints . current = [ ev . pageX , ev . pageY ] ;
464-
465- const newPositionX = Math . max (
466- panBounds . minX ,
467- Math . min ( panBounds . maxX , positionX + deltaX )
468- ) ;
469- const newPositionY = Math . max (
470- panBounds . minY ,
471- Math . min ( panBounds . maxY , positionY + deltaY )
458+ function onPointerDown ( ev : React . PointerEvent ) {
459+ // replace pointer event with the same id, if applicable
460+ pointerCache . current = pointerCache . current . filter (
461+ ( e ) => e . pointerId !== ev . pointerId
472462 ) ;
473463
474- setPositionX ( newPositionX ) ;
475- setPositionY ( newPositionY ) ;
476- }
464+ pointerCache . current . push ( ev ) ;
465+ prevDiff . current = undefined ;
477466
478- function onImageMouseDown ( ev : React . MouseEvent ) {
479- startPoints . current = [ ev . pageX , ev . pageY ] ;
480467 startTime . current = ev . timeStamp ;
481- setMoving ( true ) ;
482-
483- mouseDownEvent . current = ev . nativeEvent ;
484- }
485-
486- function onImageMouseUp ( ev : React . MouseEvent ) {
487- if ( ev . button !== 0 ) return ;
488-
489- if (
490- ! mouseDownEvent . current ||
491- ev . timeStamp - mouseDownEvent . current . timeStamp > 200
468+ if ( pointerCache . current . length === 1 ) {
469+ startPoint . current = [ ev . clientX , ev . clientY ] ;
470+ } else if (
471+ pointerCache . current . length === 2 &&
472+ startPoint . current !== undefined
492473 ) {
493- // not a click - ignore
494- return ;
474+ const centerX = Math . abs ( ev . clientX + startPoint . current [ 0 ] ) / 2 ;
475+ const centerY = Math . abs ( ev . clientY + startPoint . current [ 1 ] ) / 2 ;
476+ startPoint . current = [ centerX , centerY ] ;
495477 }
478+ }
496479
497- // must be a click
498- if (
499- ev . pageX !== startPoints . current [ 0 ] ||
500- ev . pageY !== startPoints . current [ 1 ]
501- ) {
502- return ;
503- }
480+ function onPointerUp ( ev : React . PointerEvent ) {
481+ let found = false ;
504482
505- if ( ev . nativeEvent . offsetX >= ( ev . target as HTMLElement ) . offsetWidth / 2 ) {
506- onRight ( ) ;
507- } else {
508- onLeft ( ) ;
483+ for ( let i = 0 ; i < pointerCache . current . length ; i ++ ) {
484+ if ( pointerCache . current [ i ] . pointerId === ev . pointerId ) {
485+ pointerCache . current . splice ( i , 1 ) ;
486+ found = true ;
487+ break ;
488+ }
509489 }
510- }
511490
512- const onTouchStart = useCallback (
513- ( ev : TouchEvent ) => {
514- ev . preventDefault ( ) ;
515- if ( ev . touches . length === 1 ) {
516- startPoints . current = [ ev . touches [ 0 ] . pageX , ev . touches [ 0 ] . pageY ] ;
517- startTime . current = ev . timeStamp ;
518- setMoving ( true ) ;
491+ if ( ! found || pointerCache . current . length !== 0 ) {
492+ if ( pointerCache . current . length === 1 ) {
493+ // If we are transitioning from pinch zoom to pan, reset this
494+ // so we don't pan relative to the old center point.
495+ startPoint . current = [
496+ pointerCache . current [ 0 ] . clientX ,
497+ pointerCache . current [ 0 ] . clientY ,
498+ ] ;
519499 }
520- } ,
521- [ startPoints , startTime , setMoving ]
522- ) ;
523-
524- useEffect ( ( ) => {
525- const container = containerRef . current ;
526- if ( ! container ) {
527500 return ;
528501 }
529- container . addEventListener ( "touchstart" , onTouchStart ) ;
530- return ( ) => {
531- container . removeEventListener ( "touchstart" , onTouchStart ) ;
532- } ;
533- } , [ containerRef , onTouchStart ] ) ;
534-
535- function onTouchMove ( ev : React . TouchEvent ) {
536- if ( ! moving ) return ;
537502
538- if ( ev . touches . length === 1 ) {
539- const deltaX = ev . touches [ 0 ] . pageX - startPoints . current [ 0 ] ;
540- const deltaY = ev . touches [ 0 ] . pageY - startPoints . current [ 1 ] ;
541- startPoints . current = [ ev . touches [ 0 ] . pageX , ev . touches [ 0 ] . pageY ] ;
503+ if ( ev . pointerType === "touch" && startPoint . current !== null ) {
504+ // Swipe navigation
505+ releaseCarousel ( ev , ev . timeStamp - startTime . current , false ) ;
506+ }
542507
543- if ( panBounds . minX != panBounds . maxX ) {
544- const newPositionX = Math . max (
545- panBounds . minX ,
546- Math . min ( panBounds . maxX , positionX + deltaX )
547- ) ;
548- const newPositionY = Math . max (
549- panBounds . minY ,
550- Math . min ( panBounds . maxY , positionY + deltaY )
551- ) ;
508+ if (
509+ ev . button === 0 &&
510+ ev . timeStamp - startTime . current <= 200 &&
511+ startPoint . current !== undefined &&
512+ ev . clientX === startPoint . current [ 0 ] &&
513+ ev . clientY === startPoint . current [ 1 ]
514+ ) {
515+ // Click or tap navigation
552516
553- setPositionX ( newPositionX ) ;
554- setPositionY ( newPositionY ) ;
517+ if ( ev . clientX >= window . innerWidth / 2 ) {
518+ onRight ( ) ;
555519 } else {
556- moveCarousel ( deltaX ) ;
520+ onLeft ( ) ;
557521 }
558522 }
559523 }
560524
561- function onTouchEnd ( ev : React . TouchEvent ) {
562- if ( ev . changedTouches . length === 1 && ev . touches . length === 0 ) {
563- releaseCarousel ( ev , ev . timeStamp - startTime . current ) ;
564- }
565- }
566-
567- function onPointerDown ( ev : React . PointerEvent ) {
568- // replace pointer event with the same id, if applicable
569- pointerCache . current = pointerCache . current . filter (
570- ( e ) => e . pointerId !== ev . pointerId
571- ) ;
572-
573- pointerCache . current . push ( ev ) ;
574- prevDiff . current = undefined ;
575- }
576-
577- function onPointerUp ( ev : React . PointerEvent ) {
525+ function onPointerCancel ( ev : React . PointerEvent ) {
578526 for ( let i = 0 ; i < pointerCache . current . length ; i ++ ) {
579527 if ( pointerCache . current [ i ] . pointerId === ev . pointerId ) {
580528 pointerCache . current . splice ( i , 1 ) ;
581- break ;
529+ if ( ev . pointerType === "touch" && pointerCache . current . length === 0 ) {
530+ releaseCarousel ( ev , ev . timeStamp - startTime . current , true ) ;
531+ }
532+ return ;
582533 }
583534 }
584535 }
@@ -588,19 +539,25 @@ export const LightboxImage: React.FC<IProps> = ({
588539 const cachedIndex = pointerCache . current . findIndex (
589540 ( c ) => c . pointerId === ev . pointerId
590541 ) ;
591- if ( cachedIndex !== - 1 ) {
592- pointerCache . current [ cachedIndex ] = ev ;
593- }
594542
595- if ( defaultZoom === null ) return ;
543+ if ( cachedIndex === - 1 || defaultZoom === null ) return ;
596544
597- // compare the difference between the two pointers
598- if ( pointerCache . current . length === 2 ) {
545+ pointerCache . current [ cachedIndex ] = ev ;
546+
547+ if ( pointerCache . current . length === 2 && startPoint . current !== undefined ) {
548+ // Pinch zoom
549+
550+ // compare the difference between the two pointers
599551 const ev1 = pointerCache . current [ 0 ] ;
600552 const ev2 = pointerCache . current [ 1 ] ;
601553 const diffX = Math . abs ( ev1 . clientX - ev2 . clientX ) ;
602554 const diffY = Math . abs ( ev1 . clientY - ev2 . clientY ) ;
603555 const diff = Math . sqrt ( diffX ** 2 + diffY ** 2 ) ;
556+ const centerX = Math . abs ( ev1 . clientX + ev2 . clientX ) / 2 ;
557+ const centerY = Math . abs ( ev1 . clientY + ev2 . clientY ) / 2 ;
558+ const deltaX = centerX - startPoint . current [ 0 ] ;
559+ const deltaY = centerY - startPoint . current [ 1 ] ;
560+ startPoint . current = [ centerX , centerY ] ;
604561
605562 if ( prevDiff . current !== undefined ) {
606563 const diffDiff = diff - prevDiff . current ;
@@ -609,11 +566,62 @@ export const LightboxImage: React.FC<IProps> = ({
609566 let newZoom = diffDiff > 0 ? zoom * factor : zoom / factor ;
610567 setZoom ( newZoom ) ;
611568 const bounds = calcPanBounds ( defaultZoom * newZoom ) ;
612- setPositionX ( Math . max ( bounds . minX , Math . min ( bounds . maxX , positionX ) ) ) ;
613- setPositionY ( Math . max ( bounds . minY , Math . min ( bounds . maxY , positionY ) ) ) ;
569+
570+ const newPositionX = Math . max (
571+ bounds . minX ,
572+ Math . min (
573+ bounds . maxX ,
574+ ( diffDiff > 0 ? positionX * factor : positionX / factor ) + deltaX
575+ )
576+ ) ;
577+ const newPositionY = Math . max (
578+ bounds . minY ,
579+ Math . min (
580+ bounds . maxY ,
581+ ( diffDiff > 0 ? positionY * factor : positionY / factor ) + deltaY
582+ )
583+ ) ;
584+
585+ setPositionX ( newPositionX ) ;
586+ setPositionY ( newPositionY ) ;
614587 }
615588
616589 prevDiff . current = diff ;
590+ } else if (
591+ pointerCache . current . length === 1 &&
592+ startPoint . current !== undefined &&
593+ pointerCache . current [ 0 ] . pointerType === "touch" &&
594+ panBounds . minX === panBounds . maxX
595+ ) {
596+ // Swipe navigation (touch only, and only when panning is not possible)
597+ const deltaX = ev . clientX - startPoint . current [ 0 ] ;
598+ startPoint . current = [ ev . clientX , ev . clientY ] ;
599+ moveCarousel ( deltaX ) ;
600+ } else if (
601+ pointerCache . current . length === 1 &&
602+ startPoint . current !== undefined
603+ ) {
604+ // Panning
605+
606+ if ( ! ev . buttons ) {
607+ return ;
608+ }
609+
610+ const deltaX = ev . clientX - startPoint . current [ 0 ] ;
611+ const deltaY = ev . clientY - startPoint . current [ 1 ] ;
612+ startPoint . current = [ ev . clientX , ev . clientY ] ;
613+
614+ const newPositionX = Math . max (
615+ panBounds . minX ,
616+ Math . min ( panBounds . maxX , positionX + deltaX )
617+ ) ;
618+ const newPositionY = Math . max (
619+ panBounds . minY ,
620+ Math . min ( panBounds . maxY , positionY + deltaY )
621+ ) ;
622+
623+ setPositionX ( newPositionX ) ;
624+ setPositionY ( newPositionY ) ;
617625 }
618626 }
619627
@@ -627,8 +635,10 @@ export const LightboxImage: React.FC<IProps> = ({
627635 } ) }
628636 style = { { touchAction : "none" } }
629637 onWheel = { ( e ) => onContainerScroll ( e ) }
630- onTouchMove = { onTouchMove }
631- onTouchEnd = { onTouchEnd }
638+ onPointerDown = { onPointerDown }
639+ onPointerMove = { onPointerMove }
640+ onPointerUp = { onPointerUp }
641+ onPointerCancel = { onPointerCancel }
632642 >
633643 { defaultZoom ? (
634644 < picture >
@@ -648,12 +658,6 @@ export const LightboxImage: React.FC<IProps> = ({
648658 alt = ""
649659 draggable = { false }
650660 onWheel = { current ? ( e ) => onImageScroll ( e ) : undefined }
651- onMouseDown = { onImageMouseDown }
652- onMouseUp = { onImageMouseUp }
653- onMouseMove = { onImageMouseOver }
654- onPointerDown = { onPointerDown }
655- onPointerUp = { onPointerUp }
656- onPointerMove = { onPointerMove }
657661 />
658662 </ picture >
659663 ) : undefined }
0 commit comments