diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 7641efa9..91eff098 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -63,7 +63,7 @@ const BasicFlow = () => { defaultZoom={1.5} minZoom={0.2} maxZoom={4} - fitViewOnInit + fitView > diff --git a/example/src/ControlledUncontrolled/index.tsx b/example/src/ControlledUncontrolled/index.tsx index 2cb8a0de..75a6b87c 100644 --- a/example/src/ControlledUncontrolled/index.tsx +++ b/example/src/ControlledUncontrolled/index.tsx @@ -69,7 +69,7 @@ const ControlledUncontrolled = () => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} defaultEdgeOptions={defaultEdgeOptions} - fitViewOnInit + fitView > diff --git a/example/src/CustomNode/index.tsx b/example/src/CustomNode/index.tsx index 578f68b7..5b1f883b 100644 --- a/example/src/CustomNode/index.tsx +++ b/example/src/CustomNode/index.tsx @@ -16,7 +16,7 @@ import ReactFlow, { import ColorSelectorNode from './ColorSelectorNode'; -const onPaneReady = (reactFlowInstance: ReactFlowInstance) => { +const onInit = (reactFlowInstance: ReactFlowInstance) => { console.log('flow loaded:', reactFlowInstance); reactFlowInstance.fitView(); }; @@ -111,13 +111,13 @@ const CustomNodeFlow = () => { onConnect={onConnect} onNodeDragStop={onNodeDragStop} style={{ background: bgColor }} - onPaneReady={onPaneReady} + onInit={onInit} nodeTypes={nodeTypes} connectionLineStyle={connectionLineStyle} snapToGrid={true} snapGrid={snapGrid} defaultZoom={1.5} - fitViewOnInit + fitView > { diff --git a/example/src/DefaultNodes/index.tsx b/example/src/DefaultNodes/index.tsx index 8c69926d..b0ac3f45 100644 --- a/example/src/DefaultNodes/index.tsx +++ b/example/src/DefaultNodes/index.tsx @@ -55,12 +55,7 @@ const DefaultNodes = () => { }; return ( - +
diff --git a/example/src/DragNDrop/index.tsx b/example/src/DragNDrop/index.tsx index d004d092..1c8f9ddb 100644 --- a/example/src/DragNDrop/index.tsx +++ b/example/src/DragNDrop/index.tsx @@ -31,7 +31,7 @@ const DnDFlow = () => { const [edges, setEdges, onEdgesChange] = useEdgesState([]); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); - const onPaneReady = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi); + const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi); const onDrop = (event: DragEvent) => { event.preventDefault(); @@ -60,7 +60,7 @@ const DnDFlow = () => { onEdgesChange={onEdgesChange} onNodesChange={onNodesChange} onConnect={onConnect} - onPaneReady={onPaneReady} + onInit={onInit} onDrop={onDrop} onDragOver={onDragOver} > diff --git a/example/src/EdgeTypes/index.tsx b/example/src/EdgeTypes/index.tsx index 1ed3c9c6..4d6862a0 100644 --- a/example/src/EdgeTypes/index.tsx +++ b/example/src/EdgeTypes/index.tsx @@ -15,7 +15,7 @@ import ReactFlow, { } from 'react-flow-renderer'; import { getElements } from './utils'; -const onPaneReady = (reactFlowInstance: ReactFlowInstance) => { +const onInit = (reactFlowInstance: ReactFlowInstance) => { reactFlowInstance.fitView(); console.log(reactFlowInstance.getNodes()); }; @@ -36,7 +36,7 @@ const EdgeTypesFlow = () => { edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onPaneReady={onPaneReady} + onInit={onInit} onConnect={onConnect} minZoom={0.2} zoomOnScroll={false} diff --git a/example/src/Edges/index.tsx b/example/src/Edges/index.tsx index d98e7c68..50ea0157 100644 --- a/example/src/Edges/index.tsx +++ b/example/src/Edges/index.tsx @@ -18,7 +18,7 @@ import ReactFlow, { import CustomEdge from './CustomEdge'; import CustomEdge2 from './CustomEdge2'; -const onPaneReady = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); +const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); @@ -121,7 +121,7 @@ const EdgesFlow = () => { onNodeClick={onNodeClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} - onPaneReady={onPaneReady} + onInit={onInit} snapToGrid={true} edgeTypes={edgeTypes} > diff --git a/example/src/Empty/index.tsx b/example/src/Empty/index.tsx index d7645a2b..1d6e7a6b 100644 --- a/example/src/Empty/index.tsx +++ b/example/src/Empty/index.tsx @@ -13,7 +13,7 @@ import ReactFlow, { ReactFlowInstance, } from 'react-flow-renderer'; -const onPaneReady = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance); +const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -38,7 +38,7 @@ const EmptyFlow = () => { reactFlowInstance.fitView(); +const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const { nodes: initialNodes, edges: initialEdges } = createElements(); @@ -40,7 +40,7 @@ const FloatingEdges = () => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} - onPaneReady={onPaneReady} + onInit={onInit} edgeTypes={edgeTypes} connectionLineComponent={FloatingConnectionLine} > diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx index 4f7868cf..88170feb 100644 --- a/example/src/Layouting/index.tsx +++ b/example/src/Layouting/index.tsx @@ -67,7 +67,7 @@ const LayoutFlow = () => { edges={edges} onConnect={onConnect} nodeExtent={nodeExtent} - onPaneReady={() => onLayout('TB')} + onInit={() => onLayout('TB')} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} > diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx index d213c202..c5c1d7b6 100644 --- a/example/src/NestedNodes/index.tsx +++ b/example/src/NestedNodes/index.tsx @@ -91,7 +91,7 @@ const NestedFlow = () => { const onConnect = useCallback((connection: Connection) => { setEdges((eds) => addEdge(connection, eds)); }, []); - const onPaneReady = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []); + const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []); const updatePos = () => { setNodes((nds) => { @@ -131,7 +131,7 @@ const NestedFlow = () => { { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} - fitViewOnInit + fitView > +
diff --git a/example/src/Validation/index.tsx b/example/src/Validation/index.tsx index b643db18..247302c5 100644 --- a/example/src/Validation/index.tsx +++ b/example/src/Validation/index.tsx @@ -70,7 +70,7 @@ const ValidationFlow = () => { onConnectStart={onConnectStart} onConnectStop={onConnectStop} onConnectEnd={onConnectEnd} - fitViewOnInit + fitView /> ); }; diff --git a/package-lock.json b/package-lock.json index 62a83756..fe08d4af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", "react-draggable": "^4.4.4", - "zustand": "^3.6.7" + "zustand": "^3.6.9" }, "devDependencies": { "@babel/core": "^7.16.5", diff --git a/package.json b/package.json index 7eabbee5..36646344 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", "react-draggable": "^4.4.4", - "zustand": "^3.6.7" + "zustand": "^3.6.9" }, "devDependencies": { "@babel/core": "^7.16.5", diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 04048d3e..bcf08c67 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -25,7 +25,7 @@ const Controls: FC = ({ showZoom = true, showFitView = true, showInteractive = true, - fitViewParams, + fitViewOptions, onZoomIn, onZoomOut, onFitView, @@ -51,9 +51,9 @@ const Controls: FC = ({ }, [zoomOut, onZoomOut]); const onFitViewHandler = useCallback(() => { - fitView?.(fitViewParams); + fitView?.(fitViewOptions); onFitView?.(); - }, [fitView, fitViewParams, onFitView]); + }, [fitView, fitViewOptions, onFitView]); const onInteractiveChangeHandler = useCallback(() => { store.setState({ diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 43d8fb1f..7b579d28 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -5,7 +5,7 @@ import shallow from 'zustand/shallow'; import MiniMapNode from './MiniMapNode'; import { useStore } from '../../store'; -import { getRectOfNodeInternals } from '../../utils/graph'; +import { getRectOfNodes } from '../../utils/graph'; import { getBoundsofRects } from '../../utils'; import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types'; @@ -48,7 +48,7 @@ const MiniMap = ({ const hasNodes = nodeInternals && nodeInternals.size > 0; // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); - const bb = getRectOfNodeInternals(nodes); + const bb = getRectOfNodes(nodes); const viewBB: Rect = { x: -tX / tScale, y: -tY / tScale, diff --git a/src/components/Attribution/index.tsx b/src/components/Attribution/index.tsx index e65abc4e..b87375c6 100644 --- a/src/components/Attribution/index.tsx +++ b/src/components/Attribution/index.tsx @@ -4,12 +4,12 @@ import cc from 'classcat'; import { AttributionPosition, ProOptions } from '../../types'; type AttributionProps = { - pro?: ProOptions; + proOptions?: ProOptions; position?: AttributionPosition; }; -function Attribution({ pro, position = 'bottom-right' }: AttributionProps) { - if (pro?.account === 'paid-subscription' && pro?.hideAttribution) { +function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) { + if (proOptions?.account === 'paid-subscription' && proOptions?.hideAttribution) { return null; } diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 8a15f0c4..6c9e74c0 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -40,7 +40,6 @@ export default (NodeComponent: ComponentType) => { sourcePosition, targetPosition, hidden, - isInitialized, snapToGrid, snapGrid, dragging, @@ -60,11 +59,9 @@ export default (NodeComponent: ComponentType) => { zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: hasPointerEvents ? 'all' : 'none', - // prevents jumping of nodes on start - opacity: isInitialized ? 1 : 0, ...style, }), - [zIndex, xPos, yPos, hasPointerEvents, isInitialized, style] + [zIndex, xPos, yPos, hasPointerEvents, style] ); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 3373776a..0924ff11 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -10,7 +10,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, ReactFlowState } from '../../types'; -import { getRectOfNodeInternals } from '../../utils/graph'; +import { getRectOfNodes } from '../../utils/graph'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; @@ -55,7 +55,7 @@ function NodesSelection({ [tX, tY, tScale] ); - const selectedNodesBbox = useMemo(() => getRectOfNodeInternals(selectedNodes), [selectedNodes]); + const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]); const innerStyle = useMemo( () => ({ diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index 49707d53..634e8bbd 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -17,6 +17,7 @@ import { ConnectionMode, SnapGrid, DefaultEdgeOptions, + FitViewOptions, } from '../../types'; interface StoreUpdaterProps { @@ -40,9 +41,10 @@ interface StoreUpdaterProps { snapToGrid?: boolean; snapGrid?: SnapGrid; translateExtent?: CoordinateExtent; - fitViewOnInit: boolean; connectOnClick: boolean; defaultEdgeOptions?: DefaultEdgeOptions; + fitView?: boolean; + fitViewOptions?: FitViewOptions; } const selector = (s: ReactFlowState) => ({ @@ -94,9 +96,10 @@ const StoreUpdater = ({ snapGrid, snapToGrid, translateExtent, - fitViewOnInit, connectOnClick, defaultEdgeOptions, + fitView, + fitViewOptions, }: StoreUpdaterProps) => { const { setNodes, @@ -127,12 +130,13 @@ const StoreUpdater = ({ useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState); useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState); useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState); - useDirectStoreUpdater('fitViewOnInit', fitViewOnInit, store.setState); useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState); useDirectStoreUpdater('snapGrid', snapGrid, store.setState); useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState); useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState); useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState); + useDirectStoreUpdater('fitViewOnInit', fitView, store.setState); + useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState); useStoreUpdater(nodes, setNodes); useStoreUpdater(edges, setEdges); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index a77a7212..07b4a872 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -4,7 +4,7 @@ import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import Viewport from '../Viewport'; -import useOnPaneReadyHandler from '../../hooks/useOnPaneReadyHandler'; +import useOnInitHandler from '../../hooks/useOnInitHandler'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types'; export interface GraphViewProps extends Omit { @@ -30,7 +30,7 @@ const GraphView = ({ onMove, onMoveStart, onMoveEnd, - onPaneReady, + onInit, onNodeClick, onEdgeClick, onNodeDoubleClick, @@ -82,7 +82,7 @@ const GraphView = ({ noWheelClassName, noPanClassName, }: GraphViewProps) => { - useOnPaneReadyHandler(onPaneReady); + useOnInitHandler(onInit); return ( { const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } = useStore(selector, shallow); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); - const reseizeObserverRef = useRef(); + const resizeObserverRef = useRef(); const resizeObserver = useMemo(() => { if (typeof ResizeObserver === 'undefined') { @@ -54,14 +54,14 @@ const NodeRenderer = (props: NodeRendererProps) => { updateNodeDimensions(updates); }); - reseizeObserverRef.current = observer; + resizeObserverRef.current = observer; return observer; }, []); useEffect(() => { return () => { - reseizeObserverRef?.current?.disconnect(); + resizeObserverRef?.current?.disconnect(); }; }, []); @@ -78,8 +78,6 @@ const NodeRenderer = (props: NodeRendererProps) => { const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); - const isInitialized = - node.width && node.height && typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; return ( { xPos={node.positionAbsolute?.x ?? 0} yPos={node.positionAbsolute?.y ?? 0} dragging={!!node.dragging} - isInitialized={!!isInitialized} snapGrid={snapGrid} snapToGrid={snapToGrid} selectNodesOnDrag={props.selectNodesOnDrag} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 4b822423..3c4779c7 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -46,7 +46,7 @@ const ReactFlow: FunctionComponent = forwardRef = forwardRef = forwardRef = forwardRef {onSelectionChange && } {children} - + ); diff --git a/src/hooks/useOnPaneReadyHandler.ts b/src/hooks/useOnInitHandler.ts similarity index 54% rename from src/hooks/useOnPaneReadyHandler.ts rename to src/hooks/useOnInitHandler.ts index 80a59623..28f4c8aa 100644 --- a/src/hooks/useOnPaneReadyHandler.ts +++ b/src/hooks/useOnInitHandler.ts @@ -1,19 +1,18 @@ import { useEffect, useRef } from 'react'; import useReactFlow from './useReactFlow'; -import { OnPaneReady } from '../types'; +import { OnInit } from '../types'; -function useOnPaneReadyHandler(onPaneReady: OnPaneReady | undefined) { +function useOnInitHandler(onInit: OnInit | undefined) { const ReactFlowInstance = useReactFlow(); const isInitialized = useRef(false); useEffect(() => { - if (!isInitialized.current && ReactFlowInstance.viewportInitialized && onPaneReady) { - onPaneReady(ReactFlowInstance); - + if (!isInitialized.current && ReactFlowInstance.viewportInitialized && onInit) { + setTimeout(() => onInit(ReactFlowInstance), 1); isInitialized.current = true; } - }, [onPaneReady, ReactFlowInstance]); + }, [onInit, ReactFlowInstance.viewportInitialized]); } -export default useOnPaneReadyHandler; +export default useOnInitHandler; diff --git a/src/hooks/useViewportHelper.ts b/src/hooks/useViewportHelper.ts index 9ad7f6bc..aeaabe61 100644 --- a/src/hooks/useViewportHelper.ts +++ b/src/hooks/useViewportHelper.ts @@ -1,11 +1,11 @@ import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; import shallow from 'zustand/shallow'; -import { Selection as D3Selection } from 'd3'; import { useStoreApi, useStore } from '../store'; -import { getRectOfNodeInternals, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; -import { FitViewParams, Viewport, ViewportHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types'; +import { pointToRendererPoint, getTransformForBounds, getD3Transition } from '../utils/graph'; +import { FitViewOptions, Viewport, ViewportHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types'; +import { fitView as fitViewStore } from '../store/utils'; const DEFAULT_PADDING = 0.1; @@ -16,7 +16,7 @@ const initialViewportHelper: ViewportHelperFunctions = { getZoom: () => 1, setViewport: (_: Viewport) => {}, getViewport: () => ({ x: 0, y: 0, zoom: 1 }), - fitView: (_: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {}, + fitView: (_: FitViewOptions = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {}, setCenter: (_: number, __: number) => {}, fitBounds: (_: Rect) => {}, project: (position: XYPosition) => position, @@ -28,10 +28,6 @@ const selector = (s: ReactFlowState) => ({ d3Selection: s.d3Selection, }); -const getTransition = (selection: D3Selection, duration: number = 0) => { - return selection.transition().duration(duration); -}; - const useViewportHelper = (): ViewportHelperFunctions => { const store = useStoreApi(); const { d3Zoom, d3Selection } = useStore(selector, shallow); @@ -39,45 +35,22 @@ const useViewportHelper = (): ViewportHelperFunctions => { const viewportHelperFunctions = useMemo(() => { if (d3Selection && d3Zoom) { return { - zoomIn: (options) => d3Zoom.scaleBy(getTransition(d3Selection, options?.duration), 1.2), - zoomOut: (options) => d3Zoom.scaleBy(getTransition(d3Selection, options?.duration), 1 / 1.2), - zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getTransition(d3Selection, options?.duration), zoomLevel), + zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2), + zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2), + zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel), getZoom: () => { const [, , zoom] = store.getState().transform; return zoom; }, setViewport: (transform, options) => { const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); - d3Zoom.transform(getTransition(d3Selection, options?.duration), nextTransform); + d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform); }, getViewport: () => { const [x, y, zoom] = store.getState().transform; return { x, y, zoom }; }, - fitView: (options) => { - const { nodeInternals, width, height, minZoom, maxZoom } = store.getState(); - // @TODO: work with nodeInternals instead of converting it to an array - const nodes = Array.from(nodeInternals).map(([_, node]) => node); - - if (!nodes.length) { - return; - } - - const bounds = getRectOfNodeInternals( - options?.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden) - ); - const [x, y, zoom] = getTransformForBounds( - bounds, - width, - height, - options?.minZoom ?? minZoom, - options?.maxZoom ?? maxZoom, - options?.padding ?? DEFAULT_PADDING - ); - const transform = zoomIdentity.translate(x, y).scale(zoom); - - d3Zoom.transform(getTransition(d3Selection, options?.duration), transform); - }, + fitView: (options) => fitViewStore(store.getState, options), setCenter: (x, y, options) => { const { width, height, maxZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; @@ -85,21 +58,14 @@ const useViewportHelper = (): ViewportHelperFunctions => { const centerY = height / 2 - y * nextZoom; const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom); - d3Zoom.transform(getTransition(d3Selection, options?.duration), transform); + d3Zoom.transform(getD3Transition(d3Selection, options?.duration), transform); }, fitBounds: (bounds, options) => { const { width, height, minZoom, maxZoom } = store.getState(); - const [x, y, zoom] = getTransformForBounds( - bounds, - width, - height, - minZoom, - maxZoom, - options?.padding ?? DEFAULT_PADDING - ); + const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1); const transform = zoomIdentity.translate(x, y).scale(zoom); - d3Zoom.transform(getTransition(d3Selection, options?.duration), transform); + d3Zoom.transform(getD3Transition(d3Selection, options?.duration), transform); }, project: (position: XYPosition) => { const { transform, snapToGrid, snapGrid } = store.getState(); diff --git a/src/store/index.ts b/src/store/index.ts index a4f08ab5..549f6459 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -3,6 +3,7 @@ import createContext from 'zustand/context'; import { clampPosition, getDimensions } from '../utils'; import { applyNodeChanges } from '../utils/changes'; + import { ReactFlowState, Node, @@ -20,10 +21,10 @@ import { createSelectionChange, getSelectionChanges } from '../utils/changes'; import { createNodeInternals, createPositionChange, - fitView, handleControlledEdgeSelectionChange, handleControlledNodeSelectionChange, isParentSelected, + fitView, } from './utils'; import initialState from './initialState'; @@ -54,7 +55,7 @@ const createStore = () => set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, transform, nodeInternals, fitViewOnInit } = get(); + const { onNodesChange, transform, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get(); const changes: NodeDimensionChange[] = updates.reduce((res, update) => { const node = nodeInternals.get(update.id); @@ -86,8 +87,9 @@ const createStore = () => return res; }, []); - const fitViewOnInitDone = fitViewOnInit && fitView(get); - set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone }); + const nextFitViewOnInitDone = + fitViewOnInit && !fitViewOnInitDone && fitView(get, { initial: true, ...fitViewOnInitOptions }); + set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone: nextFitViewOnInitDone }); if (changes?.length > 0) { onNodesChange?.(changes); @@ -186,6 +188,7 @@ const createStore = () => onEdgesChange?.(edgesToUnselect); } }, + setMinZoom: (minZoom: number) => { const { d3Zoom, maxZoom } = get(); d3Zoom?.scaleExtent([minZoom, maxZoom]); diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 04a358e9..e5280f43 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -37,14 +37,14 @@ const initialState: ReactFlowStore = { nodesDraggable: true, nodesConnectable: true, elementsSelectable: true, + fitViewOnInit: false, + fitViewOnInitDone: false, + fitViewOnInitOptions: undefined, multiSelectionActive: false, reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', - fitViewOnInit: false, - fitViewOnInitDone: false, - connectionStartHandle: null, connectOnClick: true, }; diff --git a/src/store/utils.ts b/src/store/utils.ts index b25be0cd..a41799f8 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3-zoom'; import { GetState } from 'zustand'; import { clampPosition, isNumeric } from '../utils'; -import { getRectOfNodes, getTransformForBounds } from '../utils/graph'; +import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph'; import { CoordinateExtent, Edge, @@ -14,6 +14,7 @@ import { ReactFlowState, XYPosition, XYZPosition, + FitViewOptions, } from '../types'; type ParentNodes = Record; @@ -145,26 +146,46 @@ export function createPositionChange({ return change; } -export function fitView(get: GetState) { +type InternalFitViewOptions = { + initial?: boolean; +} & FitViewOptions; + +export function fitView(get: GetState, options: InternalFitViewOptions = {}) { let { nodeInternals, width, height, minZoom, maxZoom, d3Zoom, d3Selection, fitViewOnInitDone, fitViewOnInit } = get(); - if (fitViewOnInit && !fitViewOnInitDone && d3Zoom && d3Selection) { - const rootNodes = Array.from(nodeInternals) - .filter(([_, n]) => !n.parentNode) - .map(([_, n]) => n); - const nodesInitialized = rootNodes.every((n) => n.width && n.height); + if ((options.initial && !fitViewOnInitDone && fitViewOnInit) || !options.initial) { + if (d3Zoom && d3Selection) { + const nodes = Array.from(nodeInternals.values()).filter((n) => + options.includeHiddenNodes ? !n.parentNode && n.width && n.height : !n.parentNode && !n.hidden + ); - if (rootNodes.length > 0 && nodesInitialized) { - const bounds = getRectOfNodes(rootNodes); - const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom ?? 0.5, maxZoom ?? 2); + const nodesInitialized = nodes.every((n) => n.width && n.height); - const nextTransform = zoomIdentity.translate(x, y).scale(zoom); - d3Zoom.transform(d3Selection, nextTransform); - fitViewOnInitDone = true; + if (nodes.length > 0 && nodesInitialized) { + const bounds = getRectOfNodes(nodes); + const [x, y, zoom] = getTransformForBounds( + bounds, + width, + height, + options.minZoom ?? minZoom, + options.maxZoom ?? maxZoom, + options.padding ?? 0.1 + ); + + const nextTransform = zoomIdentity.translate(x, y).scale(zoom); + + if (typeof options.duration === 'number' && options.duration > 0) { + d3Zoom.transform(getD3Transition(d3Selection, options.duration), nextTransform); + } else { + d3Zoom.transform(d3Selection, nextTransform); + } + + return true; + } } } - return fitViewOnInitDone; + return false; } export function handleControlledNodeSelectionChange(nodeChanges: NodeSelectionChange[], nodeInternals: NodeInternals) { diff --git a/src/types/component-props.ts b/src/types/component-props.ts index f3e1ef6f..0223d548 100644 --- a/src/types/component-props.ts +++ b/src/types/component-props.ts @@ -20,15 +20,15 @@ import { OnEdgeUpdateFunc, NodeChange, EdgeChange, - OnPaneReady, + OnInit, ProOptions, AttributionPosition, DefaultEdgeOptions, - FitViewParams, + FitViewOptions, BackgroundVariant, } from '.'; -export interface ReactFlowProps extends Omit, 'onPaneReady'> { +export interface ReactFlowProps extends HTMLAttributes { nodes?: Node[]; edges?: Edge[]; defaultNodes?: Node[]; @@ -50,7 +50,7 @@ export interface ReactFlowProps extends Omit, 'on onConnectStart?: OnConnectStart; onConnectStop?: OnConnectStop; onConnectEnd?: OnConnectEnd; - onPaneReady?: OnPaneReady; + onInit?: OnInit; onMove?: (viewport?: Viewport) => void; onMoveStart?: (viewport?: Viewport) => void; onMoveEnd?: (viewport?: Viewport) => void; @@ -106,10 +106,11 @@ export interface ReactFlowProps extends Omit, 'on noDragClassName?: string; noWheelClassName?: string; noPanClassName?: string; - fitViewOnInit?: boolean; + fitView?: boolean; + fitViewOptions?: FitViewOptions; connectOnClick?: boolean; attributionPosition?: AttributionPosition; - pro?: ProOptions; + proOptions?: ProOptions; } export type ReactFlowRefType = HTMLDivElement; @@ -129,7 +130,7 @@ export interface ControlProps extends HTMLAttributes { showZoom?: boolean; showFitView?: boolean; showInteractive?: boolean; - fitViewParams?: FitViewParams; + fitViewOptions?: FitViewOptions; onZoomIn?: () => void; onZoomOut?: () => void; onFitView?: () => void; diff --git a/src/types/general.ts b/src/types/general.ts index fd45b6ef..271ed4de 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -12,7 +12,7 @@ import { ReactFlowInstance } from './instance'; export type NodeTypesType = { [key: string]: ReactNode }; export type EdgeTypesType = NodeTypesType; -export type FitView = (fitViewOptions?: FitViewParams) => void; +export type FitView = (fitViewOptions?: FitViewOptions) => void; export type Project = (position: XYPosition) => XYPosition; @@ -28,9 +28,7 @@ export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionO export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void; export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void; -export type OnPaneReady = ( - reactFlowInstance: ReactFlowInstance -) => void; +export type OnInit = (reactFlowInstance: ReactFlowInstance) => void; export interface Connection { source: string | null; @@ -46,7 +44,7 @@ export enum ConnectionMode { export type OnConnect = (connection: Connection) => void; -export type FitViewParams = { +export type FitViewOptions = { padding?: number; includeHiddenNodes?: boolean; minZoom?: number; @@ -153,9 +151,6 @@ export type ReactFlowStore = { reactFlowVersion: string; - fitViewOnInit: boolean; - fitViewOnInitDone: boolean; - connectionStartHandle: StartHandle | null; onConnect?: OnConnect; @@ -165,6 +160,10 @@ export type ReactFlowStore = { connectOnClick: boolean; defaultEdgeOptions?: DefaultEdgeOptions; + + fitViewOnInit: boolean; + fitViewOnInitDone: boolean; + fitViewOnInitOptions: FitViewOptions | undefined; }; export type ReactFlowActions = { diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 84de1022..28ecf914 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -81,7 +81,6 @@ export interface WrapNodeProps { sourcePosition?: Position; targetPosition?: Position; hidden?: boolean; - isInitialized?: boolean; snapToGrid?: boolean; snapGrid?: SnapGrid; dragging: boolean; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 83df4bbd..e6457789 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,3 +1,4 @@ +import { Selection as D3Selection } from 'd3'; import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils'; import { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect, NodeInternals } from '../types'; @@ -124,21 +125,18 @@ export const pointToRendererPoint = ( return position; }; -// @TODO: use one function for getRectOfNodes and getRectOfNodeInternals export const getRectOfNodes = (nodes: Node[]): Rect => { const box = nodes.reduce( - (currBox, { position, width, height }) => - getBoundsOfBoxes(currBox, rectToBox({ ...position, width: width || 0, height: height || 0 })), - { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } - ); - - return boxToRect(box); -}; - -export const getRectOfNodeInternals = (nodes: Node[]): Rect => { - const box = nodes.reduce( - (currBox, { positionAbsolute, width, height }) => - getBoundsOfBoxes(currBox, rectToBox({ ...positionAbsolute!, width: width || 0, height: height || 0 })), + (currBox, { positionAbsolute, position, width, height }) => + getBoundsOfBoxes( + currBox, + rectToBox({ + x: positionAbsolute ? positionAbsolute.x : position.x, + y: positionAbsolute ? positionAbsolute.y : position.y, + width: width || 0, + height: height || 0, + }) + ), { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } ); @@ -213,3 +211,7 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; + +export const getD3Transition = (selection: D3Selection, duration: number = 0) => { + return selection.transition().duration(duration); +};