From 3d6937b3e4e6952bbbc52b2e09aa1b1ab35d3f3c Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 25 Jan 2023 17:47:13 +0100 Subject: [PATCH] refactor(connectionline): separate edge renderer and connectionline --- .../vite-app/src/examples/Basic/index.tsx | 3 - .../CustomConnectionLine/ConnectionLine.tsx | 9 +- .../vite-app/src/examples/Switch/index.tsx | 3 +- .../src/examples/Undirectional/index.tsx | 3 +- .../src/components/ConnectionLine/index.tsx | 131 +++++++++++------- .../core/src/container/EdgeRenderer/index.tsx | 130 +++++++---------- .../core/src/container/EdgeRenderer/utils.ts | 32 ++--- 7 files changed, 152 insertions(+), 159 deletions(-) diff --git a/examples/vite-app/src/examples/Basic/index.tsx b/examples/vite-app/src/examples/Basic/index.tsx index 1197ef06..52d22025 100644 --- a/examples/vite-app/src/examples/Basic/index.tsx +++ b/examples/vite-app/src/examples/Basic/index.tsx @@ -48,8 +48,6 @@ const initialEdges: Edge[] = [ { id: 'e1-3', source: '1', target: '3' }, ]; -const nodeOrigin: NodeOrigin = [0.5, 0.5]; - const defaultEdgeOptions = {}; const BasicFlow = () => { @@ -96,7 +94,6 @@ const BasicFlow = () => { selectNodesOnDrag={false} elevateEdgesOnSelect elevateNodesOnSelect={false} - // nodeOrigin={nodeOrigin} > diff --git a/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx b/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx index 4fe25917..b27306dd 100644 --- a/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx +++ b/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx @@ -1,9 +1,8 @@ -import React, { FC } from 'react'; import { ConnectionLineComponentProps } from 'reactflow'; -const ConnectionLine: FC = ({ fromX, fromY, toX, toY }) => { +function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) { return ( - + <> = ({ fromX, fromY, toX, t d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`} /> - + ); -}; +} export default ConnectionLine; diff --git a/examples/vite-app/src/examples/Switch/index.tsx b/examples/vite-app/src/examples/Switch/index.tsx index 658082ed..b6ae2278 100644 --- a/examples/vite-app/src/examples/Switch/index.tsx +++ b/examples/vite-app/src/examples/Switch/index.tsx @@ -1,5 +1,5 @@ import { MouseEvent, useCallback } from 'react'; -import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState, NodeOrigin } from 'reactflow'; +import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); @@ -100,7 +100,6 @@ const BasicFlow = () => { onNodeClick={onNodeClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} - disableKeyboardA11y onNodeDrag={onNodeDrag} >
diff --git a/examples/vite-app/src/examples/Undirectional/index.tsx b/examples/vite-app/src/examples/Undirectional/index.tsx index 4ea676a5..4f25f4a5 100644 --- a/examples/vite-app/src/examples/Undirectional/index.tsx +++ b/examples/vite-app/src/examples/Undirectional/index.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useCallback } from 'react'; +import { MouseEvent, useCallback } from 'react'; import ReactFlow, { useReactFlow, NodeTypes, @@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => { const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els)); + const { project } = useReactFlow(); const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)); diff --git a/packages/core/src/components/ConnectionLine/index.tsx b/packages/core/src/components/ConnectionLine/index.tsx index 3693f769..fae0c3b1 100644 --- a/packages/core/src/components/ConnectionLine/index.tsx +++ b/packages/core/src/components/ConnectionLine/index.tsx @@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { internalsSymbol } from '../../utils'; -import type { ConnectionLineComponent, HandleType, ReactFlowStore } from '../../types'; +import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types'; import { Position, ConnectionLineType, ConnectionMode } from '../../types'; type ConnectionLineProps = { - connectionNodeId: string; - connectionHandleType: HandleType; - connectionLineType: ConnectionLineType; - isConnectable: boolean; - connectionLineStyle?: CSSProperties; - CustomConnectionLineComponent?: ConnectionLineComponent; + nodeId: string; + handleType: HandleType; + type: ConnectionLineType; + style?: CSSProperties; + CustomComponent?: ConnectionLineComponent; }; const oppositePosition = { @@ -26,69 +25,62 @@ const oppositePosition = { }; const ConnectionLine = ({ - connectionNodeId, - connectionHandleType, - connectionLineStyle, - connectionLineType = ConnectionLineType.Bezier, - isConnectable, - CustomConnectionLineComponent, + nodeId, + handleType, + style, + type = ConnectionLineType.Bezier, + CustomComponent, }: ConnectionLineProps) => { const { fromNode, handleId, toX, toY, connectionMode } = useStore( useCallback( (s: ReactFlowStore) => ({ - fromNode: s.nodeInternals.get(connectionNodeId), + fromNode: s.nodeInternals.get(nodeId), handleId: s.connectionHandleId, toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], connectionMode: s.connectionMode, }), - [connectionNodeId] + [nodeId] ), shallow ); const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; - let handleBounds = fromHandleBounds?.[connectionHandleType]; + let handleBounds = fromHandleBounds?.[handleType]; if (connectionMode === ConnectionMode.Loose) { - handleBounds = handleBounds - ? handleBounds - : fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source']; + handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source']; } - if (!fromNode || !isConnectable || !handleBounds) { + if (!fromNode || !handleBounds) { return null; } const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; - const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2; - const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; - const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX; - const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY; - + const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0; + const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX; + const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY; const fromPosition = fromHandle?.position; + const toPosition = fromPosition ? oppositePosition[fromPosition] : null; - if (!fromPosition) { + if (!fromPosition || !toPosition) { return null; } - const toPosition: Position = oppositePosition[fromPosition]; - - if (CustomConnectionLineComponent) { + if (CustomComponent) { return ( - - - + ); } @@ -103,29 +95,62 @@ const ConnectionLine = ({ targetPosition: toPosition, }; - if (connectionLineType === ConnectionLineType.Bezier) { + if (type === ConnectionLineType.Bezier) { // we assume the destination position is opposite to the source position [dAttr] = getBezierPath(pathParams); - } else if (connectionLineType === ConnectionLineType.Step) { + } else if (type === ConnectionLineType.Step) { [dAttr] = getSmoothStepPath({ ...pathParams, borderRadius: 0, }); - } else if (connectionLineType === ConnectionLineType.SmoothStep) { + } else if (type === ConnectionLineType.SmoothStep) { [dAttr] = getSmoothStepPath(pathParams); - } else if (connectionLineType === ConnectionLineType.SimpleBezier) { + } else if (type === ConnectionLineType.SimpleBezier) { [dAttr] = getSimpleBezierPath(pathParams); } else { dAttr = `M${fromX},${fromY} ${toX},${toY}`; } - return ( - - - - ); + return ; }; ConnectionLine.displayName = 'ConnectionLine'; -export default ConnectionLine; +type ConnectionLineWrapperProps = { + type: ConnectionLineType; + component?: ConnectionLineComponent; + containerStyle?: CSSProperties; + style?: CSSProperties; +}; + +const selector = (s: ReactFlowState) => ({ + nodeId: s.connectionNodeId, + handleType: s.connectionHandleType, + nodesConnectable: s.nodesConnectable, + width: s.width, + height: s.height, +}); + +function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { + const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow); + const isValid = !!(nodeId && handleType && width && nodesConnectable); + + if (!isValid) { + return null; + } + + return ( + + + + + + ); +} + +export default ConnectionLineWrapper; diff --git a/packages/core/src/container/EdgeRenderer/index.tsx b/packages/core/src/container/EdgeRenderer/index.tsx index c687a8d8..e25f305d 100644 --- a/packages/core/src/container/EdgeRenderer/index.tsx +++ b/packages/core/src/container/EdgeRenderer/index.tsx @@ -1,27 +1,19 @@ -import { memo } from 'react'; +import { memo, ReactNode } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; import { useStore } from '../../hooks/useStore'; import useVisibleEdges from '../../hooks/useVisibleEdges'; -import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; import { getEdgePositions, getHandle, getNodeData } from './utils'; import { GraphViewProps } from '../GraphView'; -import { devWarn } from '../../utils'; import { ConnectionMode, Position } from '../../types'; import type { Edge, ReactFlowState } from '../../types'; type EdgeRendererProps = Pick< GraphViewProps, | 'edgeTypes' - | 'connectionLineType' - | 'connectionLineType' - | 'connectionLineStyle' - | 'connectionLineComponent' - | 'connectionLineContainerStyle' - | 'connectionLineContainerStyle' | 'onEdgeClick' | 'onEdgeDoubleClick' | 'defaultMarkerColor' @@ -40,11 +32,10 @@ type EdgeRendererProps = Pick< | 'disableKeyboardA11y' > & { elevateEdgesOnSelect: boolean; + children: ReactNode; }; const selector = (s: ReactFlowState) => ({ - connectionNodeId: s.connectionNodeId, - connectionHandleType: s.connectionHandleType, nodesConnectable: s.nodesConnectable, edgesFocusable: s.edgesFocusable, elementsSelectable: s.elementsSelectable, @@ -52,35 +43,38 @@ const selector = (s: ReactFlowState) => ({ height: s.height, connectionMode: s.connectionMode, nodeInternals: s.nodeInternals, + onError: s.onError, }); -const EdgeRenderer = (props: EdgeRendererProps) => { - const { - connectionNodeId, - connectionHandleType, - nodesConnectable, - edgesFocusable, - elementsSelectable, - width, - height, - connectionMode, - nodeInternals, - } = useStore(selector, shallow); - const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect); +const EdgeRenderer = ({ + defaultMarkerColor, + onlyRenderVisibleElements, + elevateEdgesOnSelect, + rfId, + edgeTypes, + noPanClassName, + onEdgeUpdate, + onEdgeContextMenu, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + onEdgeClick, + edgeUpdaterRadius, + onEdgeDoubleClick, + onEdgeUpdateStart, + onEdgeUpdateEnd, + children, +}: EdgeRendererProps) => { + const { edgesFocusable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = useStore( + selector, + shallow + ); + const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect); if (!width) { return null; } - const { - connectionLineType, - defaultMarkerColor, - connectionLineStyle, - connectionLineComponent, - connectionLineContainerStyle, - } = props; - const renderConnectionLine = connectionNodeId && connectionHandleType; - return ( <> {edgeTree.map(({ level, edges, isMaxLevel }) => ( @@ -91,11 +85,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => { height={height} className="react-flow__edges react-flow__container" > - {isMaxLevel && } + {isMaxLevel && } {edges.map((edge: Edge) => { - const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!); - const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!); + const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)); + const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)); if (!sourceIsValid || !targetIsValid) { return null; @@ -103,31 +97,29 @@ const EdgeRenderer = (props: EdgeRendererProps) => { let edgeType = edge.type || 'default'; - if (!props.edgeTypes[edgeType]) { - devWarn( - `Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300` - ); - + if (!edgeTypes[edgeType]) { + onError?.('003', `Edge type "${edgeType}" not found. Using fallback type "default".`); edgeType = 'default'; } - const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; - // when connection type is loose we can define all handles as sources + const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; + // when connection type is loose we can define all handles as sources and connect source -> source const targetNodeHandles = connectionMode === ConnectionMode.Strict ? targetHandleBounds!.target : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); - const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null); - const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null); + const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); + const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); const sourcePosition = sourceHandle?.position || Position.Bottom; const targetPosition = targetHandle?.position || Position.Top; const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); if (!sourceHandle || !targetHandle) { - devWarn( - `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: ${ + onError?.( + '008', + `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${ !sourceHandle ? edge.sourceHandle : edge.targetHandle - }; edge id: ${edge.id}. Help: https://reactflow.dev/error#800` + }", edge id: ${edge.id}.` ); return null; @@ -146,7 +138,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { { sourcePosition={sourcePosition} targetPosition={targetPosition} elementsSelectable={elementsSelectable} - onEdgeUpdate={props.onEdgeUpdate} - onContextMenu={props.onEdgeContextMenu} - onMouseEnter={props.onEdgeMouseEnter} - onMouseMove={props.onEdgeMouseMove} - onMouseLeave={props.onEdgeMouseLeave} - onClick={props.onEdgeClick} - edgeUpdaterRadius={props.edgeUpdaterRadius} - onEdgeDoubleClick={props.onEdgeDoubleClick} - onEdgeUpdateStart={props.onEdgeUpdateStart} - onEdgeUpdateEnd={props.onEdgeUpdateEnd} - rfId={props.rfId} + onEdgeUpdate={onEdgeUpdate} + onContextMenu={onEdgeContextMenu} + onMouseEnter={onEdgeMouseEnter} + onMouseMove={onEdgeMouseMove} + onMouseLeave={onEdgeMouseLeave} + onClick={onEdgeClick} + edgeUpdaterRadius={edgeUpdaterRadius} + onEdgeDoubleClick={onEdgeDoubleClick} + onEdgeUpdateStart={onEdgeUpdateStart} + onEdgeUpdateEnd={onEdgeUpdateEnd} + rfId={rfId} ariaLabel={edge.ariaLabel} isFocusable={isFocusable} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined} @@ -193,23 +185,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { ))} - {renderConnectionLine && ( - - - - )} + {children} ); }; diff --git a/packages/core/src/container/EdgeRenderer/utils.ts b/packages/core/src/container/EdgeRenderer/utils.ts index eee01706..edd74543 100644 --- a/packages/core/src/container/EdgeRenderer/utils.ts +++ b/packages/core/src/container/EdgeRenderer/utils.ts @@ -72,21 +72,18 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: Ha } } -export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null { +export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null { if (!bounds) { return null; } - // there is no handleId when there are no multiple handles/ handles with ids - // so we just pick the first one - let handle: HandleElement | null = null; - if (bounds.length === 1 || !handleId) { - handle = bounds[0]; - } else if (handleId) { - handle = bounds.find((d) => d.id === handleId)!; + if (handleId) { + return bounds.find((d) => d.id === handleId)!; + } else if (bounds.length === 1) { + return bounds[0]; } - return typeof handle === 'undefined' ? null : handle; + return null; } interface EdgePositions { @@ -167,16 +164,15 @@ export function isEdgeVisible({ return overlappingArea > 0; } -export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean] { +export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] { const handleBounds = node?.[internalsSymbol]?.handleBounds || null; - const isInvalid = - !node || - !handleBounds || - !node.width || - !node.height || - typeof node.positionAbsolute?.x === 'undefined' || - typeof node.positionAbsolute?.y === 'undefined'; + const isValid = + handleBounds && + node?.width && + node?.height && + typeof node?.positionAbsolute?.x !== 'undefined' && + typeof node?.positionAbsolute?.y !== 'undefined'; return [ { @@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean height: node?.height || 0, }, handleBounds, - !isInvalid, + !!isValid, ]; }