Merge branch 'main' into feature/edgeUpdateWithMoreInfo-1961

This commit is contained in:
Moritz Klack
2022-04-11 13:28:45 +02:00
committed by GitHub
162 changed files with 34920 additions and 27001 deletions
@@ -1,52 +1,81 @@
import React, { ReactNode } from 'react';
import React, { useMemo } from 'react';
interface MarkerProps {
import { useStore } from '../../store';
import { EdgeMarker, ReactFlowState } from '../../types';
import { getMarkerId } from '../../utils/graph';
import { useMarkerSymbol } from './MarkerSymbols';
interface MarkerProps extends EdgeMarker {
id: string;
children: ReactNode;
}
const Marker = ({ id, children }: MarkerProps) => (
<marker
className="react-flow__arrowhead"
id={id}
markerWidth="12.5"
markerHeight="12.5"
viewBox="-10 -10 20 20"
orient="auto"
refX="0"
refY="0"
>
{children}
</marker>
);
interface MarkerDefinitionsProps {
color: string;
defaultColor: string;
}
const MarkerDefinitions = ({ color }: MarkerDefinitionsProps) => {
const Marker = ({
id,
type,
color,
width = 12.5,
height = 12.5,
markerUnits = 'strokeWidth',
strokeWidth,
orient = 'auto',
}: MarkerProps) => {
const Symbol = useMarkerSymbol(type);
return (
<marker
className="react-flow__arrowhead"
id={id}
markerWidth={`${width}`}
markerHeight={`${height}`}
viewBox="-10 -10 20 20"
markerUnits={markerUnits}
orient={orient}
refX="0"
refY="0"
>
<Symbol color={color} strokeWidth={strokeWidth} />
</marker>
);
};
const edgesSelector = (s: ReactFlowState) => s.edges;
const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => {
const edges = useStore(edgesSelector);
const markers = useMemo(() => {
const ids: string[] = [];
return edges.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker);
if (!ids.includes(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId);
}
}
});
return markers.sort((a, b) => a.id.localeCompare(b.id));
}, []);
}, [edges, defaultColor]);
return (
<defs>
<Marker id="react-flow__arrowclosed">
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
{markers.map((marker: MarkerProps) => (
<Marker
id={marker.id}
key={marker.id}
type={marker.type}
color={marker.color}
width={marker.width}
height={marker.height}
markerUnits={marker.markerUnits}
strokeWidth={marker.strokeWidth}
orient={marker.orient}
/>
</Marker>
<Marker id="react-flow__arrow">
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
fill="none"
points="-5,-4 0,0 -5,4"
/>
</Marker>
))}
</defs>
);
};
@@ -0,0 +1,51 @@
import React, { useMemo } from 'react';
import { MarkerType, EdgeMarker } from '../../types';
type SymbolProps = Omit<EdgeMarker, 'type'>;
const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill="none"
points="-5,-4 0,0 -5,4"
/>
);
};
const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
/>
);
};
export const MarkerSymbols = {
[MarkerType.Arrow]: ArrowSymbol,
[MarkerType.ArrowClosed]: ArrowClosedSymbol,
};
export function useMarkerSymbol(type: MarkerType) {
const symbol = useMemo(() => {
const symbolExists = MarkerSymbols.hasOwnProperty(type);
if (!symbolExists) {
console.warn(`marker type "${type}" doesn't exist.`);
return () => null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
export default MarkerSymbols;
+146 -209
View File
@@ -1,34 +1,31 @@
import React, { memo, CSSProperties, useCallback } from 'react';
import React, { memo, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import {
Position,
Edge,
Node,
Elements,
Connection,
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
Transform,
OnEdgeUpdateFunc,
HandleType,
ReactFlowState,
} from '../../types';
import useVisibleEdges from '../../hooks/useVisibleEdges';
interface EdgeRendererProps {
edgeTypes: any;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionMode?: ConnectionMode;
onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
arrowHeadColor: string;
markerEndId?: string;
defaultMarkerColor: string;
onlyRenderVisibleElements: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
@@ -38,215 +35,155 @@ interface EdgeRendererProps {
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
noPanClassName?: string;
}
interface EdgeWrapperProps {
edge: Edge;
props: EdgeRendererProps;
nodes: Node[];
selectedElements: Elements | null;
elementsSelectable: boolean;
transform: Transform;
width: number;
height: number;
onlyRenderVisibleElements: boolean;
connectionMode?: ConnectionMode;
}
const Edge = ({
edge,
props,
nodes,
selectedElements,
elementsSelectable,
transform,
width,
height,
onlyRenderVisibleElements,
connectionMode,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
const onConnectEdge = useCallback(
(connection: Connection) => {
props.onEdgeUpdate?.(edge, connection);
},
[edge, props.onEdgeUpdate]
);
if (!sourceNode) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null;
}
if (!targetNode) {
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
return null;
}
// source and target node need to be initialized
if (!sourceNode.__rf.width || !targetNode.__rf.width) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
const targetNodeBounds = targetNode.__rf.handleBounds;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetNodeBounds.target
: targetNodeBounds.target || targetNodeBounds.source;
const sourceHandle = getHandle(sourceNode.__rf.handleBounds.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(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition
);
const isVisible = onlyRenderVisibleElements
? isEdgeVisible({
sourcePos: { x: sourceX, y: sourceY },
targetPos: { x: targetX, y: targetY },
width,
height,
transform,
})
: true;
if (!isVisible) {
return null;
}
const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false;
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={edge.className}
type={edge.type}
data={edge.data}
onClick={props.onElementClick}
selected={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}
arrowHeadType={edge.arrowHeadType}
source={edge.source}
target={edge.target}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
markerEndId={props.markerEndId}
isHidden={edge.isHidden}
onConnectEdge={onConnectEdge}
handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
/>
);
};
const selector = (s: ReactFlowState) => ({
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,
nodeInternals: s.nodeInternals,
});
const EdgeRenderer = (props: EdgeRendererProps) => {
const transform = useStoreState((state) => state.transform);
const nodes = useStoreState((state) => state.nodes);
const edges = useStoreState((state) => state.edges);
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
const connectionPosition = useStoreState((state) => state.connectionPosition);
const selectedElements = useStoreState((state) => state.selectedElements);
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const {
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionPosition,
nodesConnectable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals);
if (!width) {
return null;
}
const {
connectionLineType,
arrowHeadColor,
connectionLineStyle,
connectionLineComponent,
onlyRenderVisibleElements,
} = props;
const transformStyle = `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`;
const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} />
<g style={{ transform: transformStyle }}>
{edges.map((edge: Edge) => (
<Edge
key={edge.id}
edge={edge}
props={props}
nodes={nodes}
selectedElements={selectedElements}
elementsSelectable={elementsSelectable}
transform={transform}
width={width}
height={height}
onlyRenderVisibleElements={onlyRenderVisibleElements}
/>
))}
{renderConnectionLine && (
<ConnectionLine
nodes={nodes}
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
transform={transform}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} />}
<g>
{edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target);
if (!sourceIsValid || !targetIsValid) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: targetHandleBounds!.target || targetHandleBounds!.source;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}`);
return null;
}
if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, props.noPanClassName])}
type={edgeType}
data={edge.data}
selected={!!edge.selected}
animated={!!edge.animated}
hidden={!!edge.hidden}
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={edge.sourceHandle}
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
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}
/>
);
})}
{renderConnectionLine && isMaxLevel && (
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
))}
</>
);
};
+62 -44
View File
@@ -1,31 +1,32 @@
import { ComponentType } from 'react';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import { rectToBox } from '../../utils/graph';
import {
EdgeTypesType,
EdgeProps,
Position,
Node,
XYPosition,
ElementId,
EdgeTypes,
HandleElement,
NodeHandleBounds,
NodeInternals,
Position,
Rect,
Transform,
Edge,
XYPosition,
} from '../../types';
import { rectToBox } from '../../utils';
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
const standardTypes: EdgeTypesType = {
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypes;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypes {
const standardTypes: EdgeTypes = {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
};
const wrappedTypes = {} as EdgeTypesType;
const specialTypes: EdgeTypesType = Object.keys(edgeTypes)
const wrappedTypes = {} as EdgeTypes;
const specialTypes: EdgeTypes = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
@@ -39,11 +40,11 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
};
}
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__rf.position.x;
const y = (handle?.y || 0) + node.__rf.position.y;
const width = handle?.width || node.__rf.width;
const height = handle?.height || node.__rf.height;
export function getHandlePosition(position: Position, nodeRect: Rect, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) {
case Position.Top:
@@ -69,7 +70,7 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
}
}
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null {
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
if (!bounds) {
return null;
}
@@ -94,15 +95,15 @@ interface EdgePositions {
}
export const getEdgePositions = (
sourceNode: Node,
sourceNodeRect: Rect,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNode: Node,
targetNodeRect: Rect,
targetHandle: HandleElement | unknown,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
@@ -115,17 +116,31 @@ export const getEdgePositions = (
interface IsEdgeVisibleParams {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
transform: Transform;
}
export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }: IsEdgeVisibleParams): boolean {
export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x, targetPos.x),
y2: Math.max(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
@@ -150,22 +165,25 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
return overlappingArea > 0;
}
type SourceTargetNode = {
sourceNode: Node | null;
targetNode: Node | null;
};
export function getNodeData(nodeInternals: NodeInternals, nodeId: string): [Rect, NodeHandleBounds | null, boolean] {
const node = nodeInternals.get(nodeId);
const handleBounds = node?.handleBounds;
const isInvalid =
!node ||
!node.handleBounds ||
!node.width ||
!node.height ||
typeof node.positionAbsolute?.x === 'undefined' ||
typeof node.positionAbsolute?.y === 'undefined';
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
return nodes.reduce(
(res, node) => {
if (node.id === edge.source) {
res.sourceNode = node;
}
if (node.id === edge.target) {
res.targetNode = node;
}
return res;
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
{ sourceNode: null, targetNode: null } as SourceTargetNode
);
};
handleBounds || null,
!isInvalid,
];
}