diff --git a/examples/nextjs/pages/CustomNode/ColorSelectorNode.tsx b/examples/nextjs/pages/CustomNode/ColorSelectorNode.tsx index 622e2266..afc2d9d2 100644 --- a/examples/nextjs/pages/CustomNode/ColorSelectorNode.tsx +++ b/examples/nextjs/pages/CustomNode/ColorSelectorNode.tsx @@ -1,5 +1,5 @@ import React, { memo, FC, CSSProperties } from 'react'; -import { Handle, Position, NodeProps, Connection, Edge } from '@react-flow/bundle'; +import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange } from '@react-flow/bundle'; const targetHandleStyle: CSSProperties = { background: '#555' }; const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; @@ -12,6 +12,12 @@ const sourceHandleStyleB: CSSProperties = { const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); const ColorSelectorNode: FC = ({ data, isConnectable }) => { + useOnViewportChange({ + onStart: (viewport) => console.log('start', viewport), + onChange: (viewport) => console.log('change', viewport), + onEnd: (viewport) => console.log('end', viewport), + }); + return ( <> diff --git a/packages/core/src/components/SelectionListener/index.tsx b/packages/core/src/components/SelectionListener/index.tsx index 1afaf354..9fdcc31b 100644 --- a/packages/core/src/components/SelectionListener/index.tsx +++ b/packages/core/src/components/SelectionListener/index.tsx @@ -2,7 +2,7 @@ import { memo, useEffect } from 'react'; import shallow from 'zustand/shallow'; import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types'; -import { useStore } from '../../hooks/useStore'; +import { useStore, useStoreApi } from '../../hooks/useStore'; interface SelectionListenerProps { onSelectionChange: OnSelectionChangeFunc; @@ -28,10 +28,14 @@ function areEqual(objA: SelectorSlice, objB: SelectorSlice) { // This is just a helper component for calling the onSelectionChange listener. // @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component? function SelectionListener({ onSelectionChange }: SelectionListenerProps) { + const store = useStoreApi(); const { selectedNodes, selectedEdges } = useStore(selector, areEqual); useEffect(() => { - onSelectionChange({ nodes: selectedNodes, edges: selectedEdges }); + const params = { nodes: selectedNodes, edges: selectedEdges }; + + onSelectionChange(params); + store.getState().onSelectionChange?.(params); }, [selectedNodes, selectedEdges]); return null; diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index 6f4f2a40..da431997 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -155,10 +155,15 @@ const ZoomPane = ({ d3Zoom.on('zoom', null); } else if (!selectionKeyPressed) { d3Zoom.on('zoom', (event: D3ZoomEvent) => { + const { onViewportChange } = store.getState(); + store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] }); - if (onMove) { + + if (onMove || onViewportChange) { const flowTransform = eventToFlowTransform(event.transform); - onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); + + onViewportChange?.(flowTransform); + onMove?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); } }); } @@ -168,15 +173,19 @@ const ZoomPane = ({ useEffect(() => { if (d3Zoom) { d3Zoom.on('start', (event: D3ZoomEvent) => { + const { onViewportChangeStart } = store.getState(); isZoomingOrPanning.current = true; + if (event.sourceEvent?.type === 'mousedown') { store.setState({ paneDragging: true }); } - if (onMoveStart) { + + if (onMoveStart || onViewportChangeStart) { const flowTransform = eventToFlowTransform(event.transform); prevTransform.current = flowTransform; - onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); + onViewportChangeStart?.(flowTransform); + onMoveStart?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); } }); } @@ -185,14 +194,17 @@ const ZoomPane = ({ useEffect(() => { if (d3Zoom) { d3Zoom.on('end', (event: D3ZoomEvent) => { + const { onViewportChangeEnd } = store.getState(); + isZoomingOrPanning.current = false; store.setState({ paneDragging: false }); - if (onMoveEnd && viewChanged(prevTransform.current, event.transform)) { + if ((onMoveEnd || onViewportChangeEnd) && viewChanged(prevTransform.current, event.transform)) { const flowTransform = eventToFlowTransform(event.transform); prevTransform.current = flowTransform; - onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); + onViewportChangeEnd?.(flowTransform); + onMoveEnd?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); } }); } diff --git a/packages/core/src/hooks/useOnSelectionChange.ts b/packages/core/src/hooks/useOnSelectionChange.ts new file mode 100644 index 00000000..323f23a8 --- /dev/null +++ b/packages/core/src/hooks/useOnSelectionChange.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; + +import { useStoreApi } from './useStore'; +import type { OnSelectionChangeFunc } from '../types'; + +export type UseOnSelectionChangeOptions = { + onChange?: OnSelectionChangeFunc; +}; + +function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { + const store = useStoreApi(); + + useEffect(() => { + store.setState({ onSelectionChange: onChange }); + }, [onChange]); +} + +export default useOnSelectionChange; diff --git a/packages/core/src/hooks/useOnViewportChange.ts b/packages/core/src/hooks/useOnViewportChange.ts new file mode 100644 index 00000000..25727a59 --- /dev/null +++ b/packages/core/src/hooks/useOnViewportChange.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; + +import { useStoreApi } from './useStore'; +import type { OnViewportChange } from '../types'; + +export type UseOnViewportChangeOptions = { + onStart?: OnViewportChange; + onChange?: OnViewportChange; + onEnd?: OnViewportChange; +}; + +function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) { + const store = useStoreApi(); + + useEffect(() => { + store.setState({ onViewportChangeStart: onStart }); + }, [onStart]); + + useEffect(() => { + store.setState({ onViewportChange: onChange }); + }, [onChange]); + + useEffect(() => { + store.setState({ onViewportChangeEnd: onEnd }); + }, [onEnd]); +} + +export default useOnViewportChange; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0edee1cc..0f2ce6a6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,5 +40,6 @@ export { default as useViewport } from './hooks/useViewport'; export { default as useKeyPress } from './hooks/useKeyPress'; export * from './hooks/useNodesEdgesState'; export { useStore, useStoreApi } from './hooks/useStore'; +export { default as useOnViewportChange } from './hooks/useOnViewportChange'; export * from './types'; diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 0f124405..02c9bd8c 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -112,6 +112,8 @@ export type UnselectNodesAndEdgesParams = { edges?: Edge[]; }; +export type OnViewportChange = (viewport: Viewport) => void; + export interface ViewportHelperFunctions { zoomIn: ZoomInOut; zoomOut: ZoomInOut; @@ -193,6 +195,13 @@ export type ReactFlowStore = { onNodesDelete?: OnNodesDelete; onEdgesDelete?: OnEdgesDelete; + + // event handlers + onViewportChangeStart?: OnViewportChange; + onViewportChange?: OnViewportChange; + onViewportChangeEnd?: OnViewportChange; + + onSelectionChange?: OnSelectionChangeFunc; }; export type ReactFlowActions = {