304 lines
10 KiB
TypeScript
304 lines
10 KiB
TypeScript
import React, { memo, CSSProperties, useCallback } from 'react';
|
|
import shallow from 'zustand/shallow';
|
|
|
|
import { useStore } from '../../store';
|
|
import ConnectionLine from '../../components/ConnectionLine/index';
|
|
import MarkerDefinitions from './MarkerDefinitions';
|
|
import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils';
|
|
import {
|
|
Position,
|
|
Edge,
|
|
Connection,
|
|
ConnectionLineType,
|
|
ConnectionLineComponent,
|
|
ConnectionMode,
|
|
OnEdgeUpdateFunc,
|
|
ReactFlowState,
|
|
NodeHandleBounds,
|
|
} from '../../types';
|
|
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
|
|
|
interface EdgeRendererProps {
|
|
edgeTypes: any;
|
|
connectionLineType: ConnectionLineType;
|
|
connectionLineStyle?: CSSProperties;
|
|
connectionLineComponent?: ConnectionLineComponent;
|
|
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
|
|
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
|
|
defaultMarkerColor: string;
|
|
onlyRenderVisibleElements: boolean;
|
|
onEdgeUpdate?: OnEdgeUpdateFunc;
|
|
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
|
|
edgeUpdaterRadius?: number;
|
|
}
|
|
|
|
interface EdgeWrapperProps {
|
|
edge: Edge;
|
|
edgeTypes: any;
|
|
markerEndId?: string;
|
|
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
|
|
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
|
|
edgeUpdaterRadius?: number;
|
|
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void;
|
|
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
|
|
onEdgeUpdate?: OnEdgeUpdateFunc;
|
|
elementsSelectable: boolean;
|
|
connectionMode?: ConnectionMode;
|
|
sourceNodeWidth?: number | null;
|
|
sourceNodeHeight?: number | null;
|
|
sourceNodeX?: number;
|
|
sourceNodeY?: number;
|
|
sourceNodeHandleBounds?: NodeHandleBounds;
|
|
targetNodeWidth?: number | null;
|
|
targetNodeHeight?: number | null;
|
|
targetNodeX?: number;
|
|
targetNodeY?: number;
|
|
targetNodeHandleBounds?: NodeHandleBounds;
|
|
}
|
|
|
|
const Edge = memo(
|
|
({
|
|
edge,
|
|
edgeTypes,
|
|
markerEndId,
|
|
onEdgeClick,
|
|
onEdgeContextMenu,
|
|
onEdgeMouseEnter,
|
|
onEdgeMouseMove,
|
|
onEdgeMouseLeave,
|
|
edgeUpdaterRadius,
|
|
onEdgeDoubleClick,
|
|
onEdgeUpdateStart,
|
|
onEdgeUpdateEnd,
|
|
onEdgeUpdate,
|
|
connectionMode,
|
|
elementsSelectable,
|
|
sourceNodeWidth,
|
|
sourceNodeHeight,
|
|
sourceNodeX,
|
|
sourceNodeY,
|
|
sourceNodeHandleBounds,
|
|
targetNodeWidth,
|
|
targetNodeHeight,
|
|
targetNodeX,
|
|
targetNodeY,
|
|
targetNodeHandleBounds,
|
|
}: EdgeWrapperProps) => {
|
|
const sourceHandleId = edge.sourceHandle || null;
|
|
const targetHandleId = edge.targetHandle || null;
|
|
|
|
const onConnectEdge = useCallback(
|
|
(connection: Connection) => {
|
|
onEdgeUpdate?.(edge, connection);
|
|
},
|
|
[edge, onEdgeUpdate]
|
|
);
|
|
|
|
// source and target node need to be initialized
|
|
if (!sourceNodeHandleBounds || !targetNodeHandleBounds) {
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
!sourceNodeWidth ||
|
|
!sourceNodeHeight ||
|
|
typeof sourceNodeX === 'undefined' ||
|
|
typeof sourceNodeY === 'undefined'
|
|
) {
|
|
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
!targetNodeWidth ||
|
|
!targetNodeHeight ||
|
|
typeof targetNodeX === 'undefined' ||
|
|
typeof targetNodeY === 'undefined'
|
|
) {
|
|
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
|
|
return null;
|
|
}
|
|
|
|
const edgeType = edge.type || 'default';
|
|
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
|
|
// when connection type is loose we can define all handles as sources
|
|
const targetNodeHandles =
|
|
connectionMode === ConnectionMode.Strict
|
|
? targetNodeHandleBounds.target
|
|
: targetNodeHandleBounds.target || targetNodeHandleBounds.source;
|
|
const sourceHandle = getHandle(sourceNodeHandleBounds.source!, sourceHandleId);
|
|
const targetHandle = getHandle(targetNodeHandles!, targetHandleId);
|
|
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
|
|
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
|
|
|
|
if (!sourceHandle) {
|
|
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`);
|
|
return null;
|
|
}
|
|
|
|
if (!targetHandle) {
|
|
console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`);
|
|
return null;
|
|
}
|
|
|
|
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
|
|
{ x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight },
|
|
sourceHandle,
|
|
sourcePosition,
|
|
{ x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight },
|
|
targetHandle,
|
|
targetPosition
|
|
);
|
|
|
|
return (
|
|
<EdgeComponent
|
|
key={edge.id}
|
|
id={edge.id}
|
|
className={edge.className}
|
|
type={edgeType}
|
|
data={edge.data}
|
|
onClick={onEdgeClick}
|
|
isSelected={!!edge.isSelected}
|
|
animated={edge.animated}
|
|
label={edge.label}
|
|
labelStyle={edge.labelStyle}
|
|
labelShowBg={edge.labelShowBg}
|
|
labelBgStyle={edge.labelBgStyle}
|
|
labelBgPadding={edge.labelBgPadding}
|
|
labelBgBorderRadius={edge.labelBgBorderRadius}
|
|
style={edge.style}
|
|
source={edge.source}
|
|
target={edge.target}
|
|
sourceHandleId={sourceHandleId}
|
|
targetHandleId={targetHandleId}
|
|
markerEnd={edge.markerEnd}
|
|
markerStart={edge.markerStart}
|
|
sourceX={sourceX}
|
|
sourceY={sourceY}
|
|
targetX={targetX}
|
|
targetY={targetY}
|
|
sourcePosition={sourcePosition}
|
|
targetPosition={targetPosition}
|
|
elementsSelectable={elementsSelectable}
|
|
markerEndId={markerEndId}
|
|
isHidden={edge.isHidden}
|
|
onConnectEdge={onConnectEdge}
|
|
handleEdgeUpdate={typeof onEdgeUpdate !== 'undefined'}
|
|
onContextMenu={onEdgeContextMenu}
|
|
onMouseEnter={onEdgeMouseEnter}
|
|
onMouseMove={onEdgeMouseMove}
|
|
onMouseLeave={onEdgeMouseLeave}
|
|
edgeUpdaterRadius={edgeUpdaterRadius}
|
|
onEdgeDoubleClick={onEdgeDoubleClick}
|
|
onEdgeUpdateStart={onEdgeUpdateStart}
|
|
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
const selector = (s: ReactFlowState) => ({
|
|
transform: s.transform,
|
|
connectionNodeId: s.connectionNodeId,
|
|
connectionHandleId: s.connectionHandleId,
|
|
connectionHandleType: s.connectionHandleType,
|
|
connectionPosition: s.connectionPosition,
|
|
nodesConnectable: s.nodesConnectable,
|
|
elementsSelectable: s.elementsSelectable,
|
|
width: s.width,
|
|
height: s.height,
|
|
connectionMode: s.connectionMode,
|
|
nodes: s.nodes,
|
|
});
|
|
|
|
const EdgeRenderer = (props: EdgeRendererProps) => {
|
|
const {
|
|
transform,
|
|
connectionNodeId,
|
|
connectionHandleId,
|
|
connectionHandleType,
|
|
connectionPosition,
|
|
nodesConnectable,
|
|
elementsSelectable,
|
|
width,
|
|
height,
|
|
connectionMode,
|
|
nodes,
|
|
} = useStore(selector, shallow);
|
|
|
|
const edges = useVisibleEdges(props.onlyRenderVisibleElements);
|
|
|
|
if (!width) {
|
|
return null;
|
|
}
|
|
|
|
const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
|
|
const renderConnectionLine = connectionNodeId && connectionHandleType;
|
|
|
|
return (
|
|
<svg width={width} height={height} className="react-flow__edges">
|
|
<MarkerDefinitions defaultColor={defaultMarkerColor} />
|
|
<g transform={`translate(${transform[0]},${transform[1]}) scale(${transform[2]})`}>
|
|
{edges.map((edge: Edge) => {
|
|
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
|
|
return (
|
|
<Edge
|
|
key={edge.id}
|
|
edge={edge}
|
|
sourceNodeWidth={sourceNode?.width}
|
|
sourceNodeHeight={sourceNode?.height}
|
|
sourceNodeX={sourceNode?.position.x}
|
|
sourceNodeY={sourceNode?.position.y}
|
|
sourceNodeHandleBounds={sourceNode?.handleBounds}
|
|
targetNodeWidth={targetNode?.width}
|
|
targetNodeHeight={targetNode?.height}
|
|
targetNodeX={targetNode?.position.x}
|
|
targetNodeY={targetNode?.position.y}
|
|
targetNodeHandleBounds={targetNode?.handleBounds}
|
|
elementsSelectable={elementsSelectable}
|
|
onEdgeContextMenu={props.onEdgeContextMenu}
|
|
onEdgeMouseEnter={props.onEdgeMouseEnter}
|
|
onEdgeMouseMove={props.onEdgeMouseMove}
|
|
onEdgeMouseLeave={props.onEdgeMouseLeave}
|
|
edgeUpdaterRadius={props.edgeUpdaterRadius}
|
|
onEdgeDoubleClick={props.onEdgeDoubleClick}
|
|
onEdgeUpdateStart={props.onEdgeUpdateStart}
|
|
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
|
|
onEdgeUpdate={props.onEdgeUpdate}
|
|
edgeTypes={props.edgeTypes}
|
|
connectionMode={connectionMode}
|
|
/>
|
|
);
|
|
})}
|
|
{renderConnectionLine && (
|
|
<ConnectionLine
|
|
connectionNodeId={connectionNodeId!}
|
|
connectionHandleId={connectionHandleId}
|
|
connectionHandleType={connectionHandleType!}
|
|
connectionPositionX={connectionPosition.x}
|
|
connectionPositionY={connectionPosition.y}
|
|
transform={transform}
|
|
connectionLineStyle={connectionLineStyle}
|
|
connectionLineType={connectionLineType}
|
|
isConnectable={nodesConnectable}
|
|
CustomConnectionLineComponent={connectionLineComponent}
|
|
/>
|
|
)}
|
|
</g>
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
EdgeRenderer.displayName = 'EdgeRenderer';
|
|
|
|
export default memo(EdgeRenderer);
|