import React , { useEffect , useRef , ReactNode } from 'react' ; import { zoom , zoomIdentity } from 'd3-zoom' ; import { select , pointer } from 'd3-selection' ; import { clamp } from '../../utils' ; import useKeyPress from '../../hooks/useKeyPress' ; import useResizeHandler from '../../hooks/useResizeHandler' ; import { useStoreState , useStoreActions , useStore } from '../../store/hooks' ; import { FlowTransform , TranslateExtent , PanOnScrollMode , KeyCode } from '../../types' ; interface ZoomPaneProps { selectionKeyPressed : boolean ; elementsSelectable ? : boolean ; zoomOnScroll ? : boolean ; zoomOnPinch ? : boolean ; panOnScroll ? : boolean ; panOnScrollSpeed ? : number ; panOnScrollMode ? : PanOnScrollMode ; zoomOnDoubleClick ? : boolean ; paneMoveable ? : boolean ; defaultPosition ? : [ number , number ] ; defaultZoom ? : number ; translateExtent ? : TranslateExtent ; onMove ? : ( flowTransform ? : FlowTransform ) => void ; onMoveStart ? : ( flowTransform ? : FlowTransform ) => void ; onMoveEnd ? : ( flowTransform ? : FlowTransform ) => void ; zoomActivationKeyCode ? : KeyCode ; preventScrolling ? : boolean ; children : ReactNode ; } const viewChanged = ( prevTransform , eventTransform ) : boolean => prevTransform . x !== eventTransform . x || prevTransform . y !== eventTransform . y || prevTransform . zoom !== eventTransform . k ; const eventToFlowTransform = ( eventTransform ) : FlowTransform => ( { x : eventTransform . x , y : eventTransform . y , zoom : eventTransform . k , } ) ; const hasNoWheelClass = ( event ) => event . target . closest ( '.nowheel' ) ; const ZoomPane = ( { onMove , onMoveStart , onMoveEnd , zoomOnScroll = true , zoomOnPinch = true , panOnScroll = false , panOnScrollSpeed = 0.5 , panOnScrollMode = PanOnScrollMode . Free , zoomOnDoubleClick = true , selectionKeyPressed , elementsSelectable , paneMoveable = true , defaultPosition = [ 0 , 0 ] , defaultZoom = 1 , translateExtent , zoomActivationKeyCode , preventScrolling = true , children , } ) => { const zoomPane = useRef < HTMLDivElement > ( null ) ; const prevTransform = useRef < FlowTransform > ( { x : 0 , y : 0 , zoom : 0 } ) ; const store = useStore ( ) ; const d3Zoom = useStoreState ( ( s ) => s . d3Zoom ) ; const d3Selection = useStoreState ( ( s ) => s . d3Selection ) ; const d3ZoomHandler = useStoreState ( ( s ) => s . d3ZoomHandler ) ; const initD3Zoom = useStoreActions ( ( actions ) => actions . initD3Zoom ) ; const updateTransform = useStoreActions ( ( actions ) => actions . updateTransform ) ; const zoomActivationKeyPressed = useKeyPress ( zoomActivationKeyCode ) ; useResizeHandler ( zoomPane ) ; useEffect ( ( ) => { if ( zoomPane . current ) { const state = store . getState ( ) ; const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state . translateExtent ; const d3ZoomInstance = zoom ( ) . scaleExtent ( [ state . minZoom , state . maxZoom ] ) . translateExtent ( currentTranslateExtent ) ; const selection = select ( zoomPane . current as Element ) . call ( d3ZoomInstance ) ; const clampedX = clamp ( defaultPosition [ 0 ] , currentTranslateExtent [ 0 ] [ 0 ] , currentTranslateExtent [ 1 ] [ 0 ] ) ; const clampedY = clamp ( defaultPosition [ 1 ] , currentTranslateExtent [ 0 ] [ 1 ] , currentTranslateExtent [ 1 ] [ 1 ] ) ; const clampedZoom = clamp ( defaultZoom , state . minZoom , state . maxZoom ) ; const updatedTransform = zoomIdentity . translate ( clampedX , clampedY ) . scale ( clampedZoom ) ; d3ZoomInstance . transform ( selection , updatedTransform ) ; initD3Zoom ( { d3Zoom : d3ZoomInstance , d3Selection : selection , d3ZoomHandler : selection . on ( 'wheel.zoom' ) , transform : [ clampedX , clampedY , clampedZoom ] , } ) ; } } , [ ] ) ; useEffect ( ( ) => { if ( d3Selection && d3Zoom ) { if ( panOnScroll && ! zoomActivationKeyPressed ) { d3Selection . on ( 'wheel' , ( event ) => { if ( hasNoWheelClass ( event ) ) { return false ; } event . preventDefault ( ) ; event . stopImmediatePropagation ( ) ; const currentZoom = d3Selection . property ( '__zoom' ) . k || 1 ; if ( event . ctrlKey && zoomOnPinch ) { const point = pointer ( event ) ; const pinchDelta = - event . deltaY * ( event . deltaMode === 1 ? 0.05 : event . deltaMode ? 1 : 0.002 ) * 10 ; const zoom = currentZoom * Math . pow ( 2 , pinchDelta ) ; d3Zoom . scaleTo ( d3Selection , zoom , point ) ; return ; } const deltaNormalize = event . deltaMode === 1 ? 20 : 1 ; const deltaX = panOnScrollMode === PanOnScrollMode . Vertical ? 0 : event . deltaX * deltaNormalize ; const deltaY = panOnScrollMode === PanOnScrollMode . Horizontal ? 0 : event . deltaY * deltaNormalize ; d3Zoom . translateBy ( d3Selection , - ( deltaX / currentZoom ) * panOnScrollSpeed , - ( deltaY / currentZoom ) * panOnScrollSpeed ) ; } ) . on ( 'wheel.zoom' , null ) ; } else if ( typeof d3ZoomHandler !== 'undefined' ) { d3Selection . on ( 'wheel' , ( event ) => { if ( ! preventScrolling || hasNoWheelClass ( event ) ) { return null ; } event . preventDefault ( ) ; } ) . on ( 'wheel.zoom' , d3ZoomHandler ) ; } } } , [ panOnScroll , panOnScrollMode , d3Selection , d3Zoom , d3ZoomHandler , zoomActivationKeyPressed , zoomOnPinch , preventScrolling , ] ) ; useEffect ( ( ) => { if ( d3Zoom ) { if ( selectionKeyPressed ) { d3Zoom . on ( 'zoom' , null ) ; } else { d3Zoom . on ( 'zoom' , ( event ) => { updateTransform ( [ event . transform . x , event . transform . y , event . transform . k ] ) ; if ( onMove ) { const flowTransform = eventToFlowTransform ( event . transform ) ; onMove ( flowTransform ) ; } } ) ; } } } , [ selectionKeyPressed , d3Zoom , updateTransform , onMove ] ) ; useEffect ( ( ) => { if ( d3Zoom ) { if ( onMoveStart ) { d3Zoom . on ( 'start' , ( event ) => { const flowTransform = eventToFlowTransform ( event . transform ) ; prevTransform . current = flowTransform ; onMoveStart ( flowTransform ) ; } ) ; } else { d3Zoom . on ( 'start' , null ) ; } } } , [ d3Zoom , onMoveStart ] ) ; useEffect ( ( ) => { if ( d3Zoom ) { if ( onMoveEnd ) { d3Zoom . on ( 'end' , ( event ) => { if ( viewChanged ( prevTransform . current , event . transform ) ) { const flowTransform = eventToFlowTransform ( event . transform ) ; prevTransform . current = flowTransform ; onMoveEnd ( flowTransform ) ; } } ) ; } else { d3Zoom . on ( 'end' , null ) ; } } } , [ d3Zoom , onMoveEnd ] ) ; useEffect ( ( ) => { if ( d3Zoom ) { d3Zoom . filter ( ( event ) => { const zoomScroll = zoomActivationKeyPressed || zoomOnScroll ; const pinchZoom = zoomOnPinch && event . ctrlKey ; if ( ! paneMoveable && ! zoomScroll && ! panOnScroll && ! zoomOnDoubleClick && ! zoomOnPinch ) { return false ; } if ( selectionKeyPressed ) { return false ; } if ( ! zoomOnDoubleClick && event . type === 'dblclick' ) { return false ; } if ( hasNoWheelClass ( event ) && event . type === 'wheel' ) { return false ; } if ( ( event . target . closest ( '.react-flow__node' ) || event . target . closest ( '.react-flow__edge' ) ) && event . type !== 'wheel' ) { return false ; } if ( event . target . closest ( '.react-flow__nodesselection' ) && event . type !== 'wheel' ) { return false ; } if ( ! zoomOnPinch && event . ctrlKey && event . type === 'wheel' ) { return false ; } if ( ! zoomScroll && ! panOnScroll && ! pinchZoom && event . type === 'wheel' ) { return false ; } if ( ! paneMoveable && ( event . type === 'mousedown' || event . type === 'touchstart' ) ) { return false ; } return ( ! event . ctrlKey || event . type === 'wheel' ) && ! event . button ; } ) ; } } , [ d3Zoom , zoomOnScroll , zoomOnPinch , panOnScroll , zoomOnDoubleClick , paneMoveable , selectionKeyPressed , elementsSelectable , zoomActivationKeyPressed , ] ) ; return ( < div className = "react-flow__renderer react-flow__zoompane" ref = { zoomPane } > { children } < /div>); } ; export default ZoomPane ;