refactor(connectionline): separate edge renderer and connectionline

This commit is contained in:
moklick
2023-01-25 17:47:13 +01:00
parent fe0f59adf4
commit 3d6937b3e4
7 changed files with 152 additions and 159 deletions

View File

@@ -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}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />

View File

@@ -1,9 +1,8 @@
import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'reactflow';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => {
function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
return (
<g>
<>
<path
fill="none"
stroke="#222"
@@ -12,8 +11,8 @@ const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, t
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g>
</>
);
};
}
export default ConnectionLine;

View File

@@ -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}
>
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>

View File

@@ -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));

View File

@@ -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 (
<g className="react-flow__connection">
<CustomConnectionLineComponent
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
/>
</g>
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
/>
);
}
@@ -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 (
<g className="react-flow__connection">
<path d={dAttr} fill="none" className="react-flow__connection-path" style={connectionLineStyle} />
</g>
);
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
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 (
<svg
style={containerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<g className="react-flow__connection">
<ConnectionLine nodeId={nodeId} handleType={handleType} style={style} type={type} CustomComponent={component} />
</g>
</svg>
);
}
export default ConnectionLineWrapper;

View File

@@ -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 && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={props.rfId} />}
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g>
{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) => {
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, props.noPanClassName])}
className={cc([edge.className, noPanClassName])}
type={edgeType}
data={edge.data}
selected={!!edge.selected}
@@ -172,17 +164,17 @@ 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) => {
</g>
</svg>
))}
{renderConnectionLine && (
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleType={connectionHandleType!}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
{children}
</>
);
};

View File

@@ -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,
];
}