refactor(elements): split into nodes and edges

This commit is contained in:
moklick
2021-10-09 11:33:10 +02:00
parent be3b451d1d
commit 32ee964044
23 changed files with 502 additions and 393 deletions
@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth, strokeWidth,
className, className,
borderRadius, borderRadius,
shapeRendering shapeRendering,
}: MiniMapNodeProps) => { }: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {}; const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string; const fill = (color || background || backgroundColor) as string;
+9 -9
View File
@@ -41,9 +41,9 @@ const MiniMap = ({
const elementWidth = (style?.width || defaultWidth)! as number; const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number; const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function const nodeStrokeColorFunc = (
? nodeStrokeColor nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
: () => nodeStrokeColor) as StringFunc; ) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
const hasNodes = nodes && nodes.length; const hasNodes = nodes && nodes.length;
const bb = getRectOfNodes(nodes); const bb = getRectOfNodes(nodes);
@@ -64,7 +64,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2; const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2; const height = viewHeight + offset * 2;
const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision"; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
return ( return (
<svg <svg
@@ -75,14 +75,14 @@ const MiniMap = ({
className={mapClasses} className={mapClasses}
> >
{nodes {nodes
.filter((node) => !node.isHidden) .filter((node) => !node.isHidden && node.width && node.height)
.map((node) => ( .map((node) => (
<MiniMapNode <MiniMapNode
key={node.id} key={node.id}
x={node.__rf.position.x} x={node.position.x}
y={node.__rf.position.y} y={node.position.y}
width={node.__rf.width} width={node.width!}
height={node.__rf.height} height={node.height!}
style={node.style} style={node.style}
className={nodeClassNameFunc(node)} className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)} color={nodeColorFunc(node)}
+8 -8
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState, CSSProperties } from 'react'; import React, { useEffect, useState, CSSProperties } from 'react';
import { useStoreState } from '../../store/hooks';
import { getBezierPath } from '../Edges/BezierEdge'; import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { import {
@@ -20,7 +21,6 @@ interface ConnectionLineProps {
connectionPositionX: number; connectionPositionX: number;
connectionPositionY: number; connectionPositionY: number;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform; transform: Transform;
isConnectable: boolean; isConnectable: boolean;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
@@ -35,11 +35,11 @@ export default ({
connectionPositionX, connectionPositionX,
connectionPositionY, connectionPositionY,
connectionLineType = ConnectionLineType.Bezier, connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform, transform,
isConnectable, isConnectable,
CustomConnectionLineComponent, CustomConnectionLineComponent,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const nodes = useStoreState((state) => state.nodes);
const [sourceNode, setSourceNode] = useState<Node | null>(null); const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId; const nodeId = connectionNodeId;
const handleId = connectionHandleId; const handleId = connectionHandleId;
@@ -54,12 +54,12 @@ export default ({
} }
const sourceHandle = handleId const sourceHandle = handleId
? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) ? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
: sourceNode.__rf.handleBounds[connectionHandleType][0]; : sourceNode.handleBounds[connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height; const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height;
const sourceX = sourceNode.__rf.position.x + sourceHandleX; const sourceX = sourceNode.position.x + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY; const sourceY = sourceNode.position.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2];
+12 -6
View File
@@ -1,18 +1,24 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useStoreActions } from '../../store/hooks'; import { useStoreActions } from '../../store/hooks';
import { Elements } from '../../types'; import { Node, Edge } from '../../types';
interface ElementUpdaterProps { interface ElementUpdaterProps {
elements: Elements; nodes: Node[];
edges: Edge[];
} }
const ElementUpdater = ({ elements }: ElementUpdaterProps) => { const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => {
const setElements = useStoreActions((actions) => actions.setElements); const setNodes = useStoreActions((actions) => actions.setNodes);
const setEdges = useStoreActions((actions) => actions.setEdges);
useEffect(() => { useEffect(() => {
setElements(elements); setNodes(nodes);
}, [elements]); }, [nodes]);
useEffect(() => {
setEdges(edges);
}, [edges]);
return null; return null;
}; };
+6 -2
View File
@@ -1,6 +1,9 @@
import { MouseEvent as ReactMouseEvent } from 'react'; import { MouseEvent as ReactMouseEvent } from 'react';
import { Store } from 'redux';
import { getHostForElement } from '../../utils'; import { getHostForElement } from '../../utils';
import { ReactFlowState } from '../../types';
import { ReactFlowAction } from '../../store/actions';
import { import {
ElementId, ElementId,
@@ -103,7 +106,8 @@ export function onMouseDown(
onEdgeUpdateEnd?: (evt: MouseEvent) => void, onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc, onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc, onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc onConnectEnd?: OnConnectEndFunc,
store?: Store<ReactFlowState, ReactFlowAction>
): void { ): void {
const reactFlowNode = (event.target as Element).closest('.react-flow'); const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document // when react-flow is used inside a shadow root we can't use document
@@ -177,7 +181,7 @@ export function onMouseDown(
onConnectStop?.(event); onConnectStop?.(event);
if (isValid) { if (isValid) {
onConnect?.(connection); onConnect?.(connection, store?.getState().nodes || []);
} }
onConnectEnd?.(event); onConnectEnd?.(event);
+8 -6
View File
@@ -1,9 +1,9 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { useStoreActions, useStoreState } from '../../store/hooks'; import { useStoreActions, useStoreState, useStore } from '../../store/hooks';
import NodeIdContext from '../../contexts/NodeIdContext'; import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position } from '../../types'; import { HandleProps, Connection, ElementId, Position, Node } from '../../types';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
@@ -26,6 +26,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}, },
ref ref
) => { ) => {
const store = useStore();
const nodeId = useContext(NodeIdContext) as ElementId; const nodeId = useContext(NodeIdContext) as ElementId;
const setPosition = useStoreActions((actions) => actions.setConnectionPosition); const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
@@ -38,9 +39,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const isTarget = type === 'target'; const isTarget = type === 'target';
const onConnectExtended = useCallback( const onConnectExtended = useCallback(
(params: Connection) => { (params: Connection, nodes: Node[]) => {
onConnectAction?.(params); onConnectAction?.(params, nodes);
onConnect?.(params); onConnect?.(params, nodes);
}, },
[onConnectAction, onConnect] [onConnectAction, onConnect]
); );
@@ -61,7 +62,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
undefined, undefined,
onConnectStart, onConnectStart,
onConnectStop, onConnectStop,
onConnectEnd onConnectEnd,
store
); );
}, },
[ [
+32 -32
View File
@@ -1,18 +1,8 @@
import React, { import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat'; import cc from 'classcat';
import { useStoreActions } from '../../store/hooks'; import { useStoreActions, useStoreState } from '../../store/hooks';
import { Provider } from '../../contexts/NodeIdContext'; import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types'; import { NodeComponentProps, WrapNodeProps } from '../../types';
@@ -50,9 +40,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
resizeObserver, resizeObserver,
dragHandle, dragHandle,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); const onNodesChange = useStoreState((state) => state.onNodesChange);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const nodeElement = useRef<HTMLDivElement>(null); const nodeElement = useRef<HTMLDivElement>(null);
@@ -84,6 +74,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onMouseLeave, onMouseLeave,
] ]
); );
const onMouseEnterHandler = useMemo(() => { const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) { if (!onMouseEnter || isDragging) {
return; return;
@@ -153,20 +144,25 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const onDrag = useCallback( const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => { (event: DraggableEvent, draggableData: DraggableData) => {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
if (onNodeDrag) { if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node); onNodeDrag(event as MouseEvent, node);
} }
updateNodePosDiff({ onNodesChange?.([
id, {
diff: { id,
x: draggableData.deltaX, change: {
y: draggableData.deltaY, position: {
x: node.position.x,
y: node.position.y,
},
isDragging: true,
},
}, },
isDragging: true, ]);
});
}, },
[id, node, onNodeDrag] [id, node, onNodeDrag]
); );
@@ -185,10 +181,14 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
return; return;
} }
updateNodePosDiff({ onNodesChange?.([
id: node.id, {
isDragging: false, id: node.id,
}); change: {
isDragging: true,
},
},
]);
onNodeDragStop?.(event as MouseEvent, node); onNodeDragStop?.(event as MouseEvent, node);
}, },
@@ -202,11 +202,11 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
[node, onNodeDoubleClick] [node, onNodeDoubleClick]
); );
useLayoutEffect(() => { // useEffect(() => {
if (nodeElement.current && !isHidden) { // if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); // updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
} // }
}, [id, isHidden, sourcePosition, targetPosition]); // }, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => { useEffect(() => {
if (nodeElement.current) { if (nodeElement.current) {
+1 -1
View File
@@ -45,7 +45,7 @@ export default ({
return { return {
...matchingNode, ...matchingNode,
position: matchingNode?.__rf.position, position: matchingNode?.position,
} as Node; } as Node;
}) })
: [], : [],
+164 -149
View File
@@ -4,7 +4,7 @@ import { useStoreState } from '../../store/hooks';
import ConnectionLine from '../../components/ConnectionLine/index'; import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph'; import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils'; import { getEdgePositions, getHandle } from './utils';
import { import {
Position, Position,
Edge, Edge,
@@ -14,7 +14,6 @@ import {
ConnectionLineType, ConnectionLineType,
ConnectionLineComponent, ConnectionLineComponent,
ConnectionMode, ConnectionMode,
Transform,
OnEdgeUpdateFunc, OnEdgeUpdateFunc,
} from '../../types'; } from '../../types';
@@ -41,151 +40,167 @@ interface EdgeRendererProps {
interface EdgeWrapperProps { interface EdgeWrapperProps {
edge: Edge; edge: Edge;
props: EdgeRendererProps; edgeTypes: any;
nodes: Node[]; markerEndId?: string;
onElementClick?: (event: React.MouseEvent, element: 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;
targetNode?: Node;
sourceNode?: Node;
selectedElements: Elements | null; selectedElements: Elements | null;
elementsSelectable: boolean; elementsSelectable: boolean;
transform: Transform;
width: number;
height: number;
onlyRenderVisibleElements: boolean;
connectionMode?: ConnectionMode; connectionMode?: ConnectionMode;
} }
const Edge = ({ const Edge = memo(
edge, ({
props, edge,
nodes, edgeTypes,
selectedElements, markerEndId,
elementsSelectable, onElementClick,
transform, onEdgeContextMenu,
width, onEdgeMouseEnter,
height, onEdgeMouseMove,
onlyRenderVisibleElements, onEdgeMouseLeave,
connectionMode, edgeUpdaterRadius,
}: EdgeWrapperProps) => { onEdgeDoubleClick,
const sourceHandleId = edge.sourceHandle || null; onEdgeUpdateStart,
const targetHandleId = edge.targetHandle || null; onEdgeUpdateEnd,
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); onEdgeUpdate,
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, targetNode,
targetHandle, sourceNode,
targetPosition selectedElements,
); elementsSelectable,
connectionMode,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const isVisible = onlyRenderVisibleElements const onConnectEdge = useCallback(
? isEdgeVisible({ (connection: Connection) => {
sourcePos: { x: sourceX, y: sourceY }, onEdgeUpdate?.(edge, connection);
targetPos: { x: targetX, y: targetY }, },
width, [edge, onEdgeUpdate]
height, );
transform,
})
: true;
if (!isVisible) { if (!sourceNode) {
return null; 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.width || !targetNode.width) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
const targetNodeBounds = targetNode.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.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={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={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 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 EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = (props: EdgeRendererProps) => {
const transform = useStoreState((state) => state.transform); const transform = useStoreState((state) => state.transform);
const nodes = useStoreState((state) => state.nodes);
const edges = useStoreState((state) => state.edges); const edges = useStoreState((state) => state.edges);
const connectionNodeId = useStoreState((state) => state.connectionNodeId); const connectionNodeId = useStoreState((state) => state.connectionNodeId);
const connectionHandleId = useStoreState((state) => state.connectionHandleId); const connectionHandleId = useStoreState((state) => state.connectionHandleId);
@@ -201,13 +216,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
return null; return null;
} }
const { const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props;
connectionLineType,
arrowHeadColor,
connectionLineStyle,
connectionLineComponent,
onlyRenderVisibleElements,
} = props;
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
const renderConnectionLine = connectionNodeId && connectionHandleType; const renderConnectionLine = connectionNodeId && connectionHandleType;
@@ -219,19 +228,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<Edge <Edge
key={edge.id} key={edge.id}
edge={edge} edge={edge}
props={props} sourceNode={edge.sourceNode}
nodes={nodes} targetNode={edge.targetNode}
selectedElements={selectedElements} selectedElements={selectedElements}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
transform={transform} markerEndId={props.markerEndId}
width={width} onEdgeContextMenu={props.onEdgeContextMenu}
height={height} onEdgeMouseEnter={props.onEdgeMouseEnter}
onlyRenderVisibleElements={onlyRenderVisibleElements} onEdgeMouseMove={props.onEdgeMouseMove}
onEdgeMouseLeave={props.onEdgeMouseLeave}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
onEdgeUpdate={props.onEdgeUpdate}
edgeTypes={props.edgeTypes}
/> />
))} ))}
{renderConnectionLine && ( {renderConnectionLine && (
<ConnectionLine <ConnectionLine
nodes={nodes}
connectionNodeId={connectionNodeId!} connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId} connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!} connectionHandleType={connectionHandleType!}
+2 -2
View File
@@ -40,8 +40,8 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
} }
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition { export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__rf.position.x; const x = (handle?.x || 0) + node.position.x;
const y = (handle?.y || 0) + node.__rf.position.y; const y = (handle?.y || 0) + node.position.y;
const width = handle?.width || node.__rf.width; const width = handle?.width || node.__rf.width;
const height = handle?.height || node.__rf.height; const height = handle?.height || node.__rf.height;
+1 -2
View File
@@ -30,7 +30,6 @@ const FlowRenderer = ({
onPaneClick, onPaneClick,
onPaneContextMenu, onPaneContextMenu,
onPaneScroll, onPaneScroll,
onElementsRemove,
deleteKeyCode, deleteKeyCode,
onMove, onMove,
onMoveStart, onMoveStart,
@@ -61,7 +60,7 @@ const FlowRenderer = ({
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode }); useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = useCallback( const onClick = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
+18 -3
View File
@@ -11,7 +11,7 @@ import { ReactFlowProps } from '../ReactFlow';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'elements'> { export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType; edgeTypes: EdgeTypesType;
selectionKeyCode: KeyCode; selectionKeyCode: KeyCode;
@@ -55,7 +55,6 @@ const GraphView = ({
selectionKeyCode, selectionKeyCode,
multiSelectionKeyCode, multiSelectionKeyCode,
zoomActivationKeyCode, zoomActivationKeyCode,
onElementsRemove,
deleteKeyCode, deleteKeyCode,
onConnect, onConnect,
onConnectStart, onConnectStart,
@@ -95,6 +94,8 @@ const GraphView = ({
edgeUpdaterRadius, edgeUpdaterRadius,
onEdgeUpdateStart, onEdgeUpdateStart,
onEdgeUpdateEnd, onEdgeUpdateEnd,
onNodesChange,
onEdgesChange,
}: GraphViewProps) => { }: GraphViewProps) => {
const isInitialized = useRef<boolean>(false); const isInitialized = useRef<boolean>(false);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
@@ -111,6 +112,9 @@ const GraphView = ({
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent); const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode); const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange);
const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange);
const currentStore = useStore(); const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
@@ -217,12 +221,23 @@ const GraphView = ({
} }
}, [connectionMode]); }, [connectionMode]);
useEffect(() => {
if (typeof onNodesChange !== 'undefined') {
setOnNodesChange(onNodesChange);
}
}, [onNodesChange]);
useEffect(() => {
if (typeof onEdgesChange !== 'undefined') {
setOnEdgesChange(onEdgesChange);
}
}, [onEdgesChange]);
return ( return (
<FlowRenderer <FlowRenderer
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu} onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll} onPaneScroll={onPaneScroll}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode} deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode} selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode} multiSelectionKeyCode={multiSelectionKeyCode}
+8 -11
View File
@@ -1,6 +1,5 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
import { getNodesInside } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks'; import { useStoreState, useStoreActions } from '../../store/hooks';
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types'; import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
interface NodeRendererProps { interface NodeRendererProps {
@@ -26,14 +25,12 @@ const NodeRenderer = (props: NodeRendererProps) => {
const nodesDraggable = useStoreState((state) => state.nodesDraggable); const nodesDraggable = useStoreState((state) => state.nodesDraggable);
const nodesConnectable = useStoreState((state) => state.nodesConnectable); const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable); const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const nodes = useStoreState((state) => state.nodes); const nodes = useStoreState((state) => state.nodes);
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const visibleNodes = props.onlyRenderVisibleElements // const visibleNodes = props.onlyRenderVisibleElements
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes; // : nodes;
const transformStyle = useMemo( const transformStyle = useMemo(
() => ({ () => ({
@@ -59,7 +56,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
return ( return (
<div className="react-flow__nodes" style={transformStyle}> <div className="react-flow__nodes" style={transformStyle}>
{visibleNodes.map((node) => { {nodes.map((node) => {
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
@@ -82,10 +79,10 @@ const NodeRenderer = (props: NodeRendererProps) => {
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition} targetPosition={node.targetPosition}
isHidden={node.isHidden} isHidden={node.isHidden}
xPos={node.__rf.position.x} xPos={node.position.x}
yPos={node.__rf.position.y} yPos={node.position.y}
isDragging={node.__rf.isDragging} isDragging={node.isDragging}
isInitialized={node.__rf.width !== null && node.__rf.height !== null} isInitialized={node.width !== null && node.height !== null}
snapGrid={props.snapGrid} snapGrid={props.snapGrid}
snapToGrid={props.snapToGrid} snapToGrid={props.snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag} selectNodesOnDrag={props.selectNodesOnDrag}
+19 -10
View File
@@ -25,7 +25,6 @@ import {
OnLoadFunc, OnLoadFunc,
Node, Node,
Edge, Edge,
Connection,
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
ConnectionLineComponent, ConnectionLineComponent,
@@ -33,11 +32,13 @@ import {
OnConnectStartFunc, OnConnectStartFunc,
OnConnectStopFunc, OnConnectStopFunc,
OnConnectEndFunc, OnConnectEndFunc,
OnConnectFunc,
TranslateExtent, TranslateExtent,
KeyCode, KeyCode,
PanOnScrollMode, PanOnScrollMode,
OnEdgeUpdateFunc, OnEdgeUpdateFunc,
NodeExtent, NodeExtent,
ElementChange,
} from '../../types'; } from '../../types';
import '../../style.css'; import '../../style.css';
@@ -57,9 +58,11 @@ const defaultEdgeTypes = {
}; };
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> { export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
elements: Elements; nodes: Node[];
edges: Edge[];
onNodesChange?: (nodeChanges: ElementChange[]) => void;
onEdgesChange?: (edgeChanges: ElementChange[]) => void;
onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void; onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void;
onElementsRemove?: (elements: Elements) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
@@ -68,7 +71,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onConnect?: (connection: Edge | Connection) => void; onConnect?: OnConnectFunc;
onConnectStart?: OnConnectStartFunc; onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc; onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc; onConnectEnd?: OnConnectEndFunc;
@@ -132,10 +135,14 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>( const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
( (
{ {
elements = [], nodes = [],
edges = [],
className, className,
nodeTypes = defaultNodeTypes, nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes, edgeTypes = defaultEdgeTypes,
@@ -144,7 +151,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onMove, onMove,
onMoveStart, onMoveStart,
onMoveEnd, onMoveEnd,
onElementsRemove,
onConnect, onConnect,
onConnectStart, onConnectStart,
onConnectStop, onConnectStop,
@@ -171,7 +177,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
multiSelectionKeyCode = 'Meta', multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta', zoomActivationKeyCode = 'Meta',
snapToGrid = false, snapToGrid = false,
snapGrid = [15, 15], snapGrid = initSnapGrid,
onlyRenderVisibleElements = false, onlyRenderVisibleElements = false,
selectNodesOnDrag = true, selectNodesOnDrag = true,
nodesDraggable, nodesDraggable,
@@ -180,7 +186,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
minZoom, minZoom,
maxZoom, maxZoom,
defaultZoom = 1, defaultZoom = 1,
defaultPosition = [0, 0], defaultPosition = initDefaultPosition,
translateExtent, translateExtent,
preventScrolling = true, preventScrolling = true,
nodeExtent, nodeExtent,
@@ -208,6 +214,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
edgeUpdaterRadius = 10, edgeUpdaterRadius = 10,
nodeTypesId = '1', nodeTypesId = '1',
edgeTypesId = '1', edgeTypesId = '1',
onNodesChange,
onEdgesChange,
...rest ...rest
}, },
ref ref
@@ -240,7 +248,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent} connectionLineComponent={connectionLineComponent}
selectionKeyCode={selectionKeyCode} selectionKeyCode={selectionKeyCode}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode} deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode} multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode} zoomActivationKeyCode={zoomActivationKeyCode}
@@ -287,8 +294,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/> />
<ElementUpdater elements={elements} /> <ElementUpdater nodes={nodes} edges={edges} />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />} {onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children} {children}
</Wrapper> </Wrapper>
+15 -12
View File
@@ -1,22 +1,23 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useStore, useStoreActions } from '../store/hooks'; import { useStore, useStoreActions, useStoreState } from '../store/hooks';
import useKeyPress from './useKeyPress'; import useKeyPress from './useKeyPress';
import { isNode, getConnectedEdges } from '../utils/graph'; import { isNode, isEdge, getConnectedEdges } from '../utils/graph';
import { Elements, KeyCode, ElementId, FlowElement } from '../types'; import { KeyCode } from '../types';
interface HookParams { interface HookParams {
deleteKeyCode: KeyCode; deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode; multiSelectionKeyCode: KeyCode;
onElementsRemove?: (elements: Elements) => void;
} }
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStore(); const store = useStore();
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
const onNodesChange = useStoreState((state) => state.onNodesChange);
const onEdgesChange = useStoreState((state) => state.onEdgesChange);
const deleteKeyPressed = useKeyPress(deleteKeyCode); const deleteKeyPressed = useKeyPress(deleteKeyCode);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
@@ -24,19 +25,21 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
useEffect(() => { useEffect(() => {
const { edges, selectedElements } = store.getState(); const { edges, selectedElements } = store.getState();
if (onElementsRemove && deleteKeyPressed && selectedElements) { if (deleteKeyPressed && selectedElements) {
const selectedNodes = selectedElements.filter(isNode); const selectedNodes = selectedElements.filter(isNode);
const selectedEdges = selectedElements.filter(isEdge);
const connectedEdges = getConnectedEdges(selectedNodes, edges); const connectedEdges = getConnectedEdges(selectedNodes, edges);
const elementsToRemove = [...selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),
new Map<ElementId, FlowElement>()
);
onElementsRemove(Array.from(elementsToRemove.values())); const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true }));
const edgeChanges = [...selectedEdges, ...connectedEdges].map((e) => ({ id: e.id, delete: true }));
onNodesChange?.(nodeChanges);
onEdgesChange?.(edgeChanges);
unsetNodesSelection(); unsetNodesSelection();
resetSelectedElements(); resetSelectedElements();
} }
}, [deleteKeyPressed, onElementsRemove]); }, [deleteKeyPressed, onNodesChange, onEdgesChange]);
useEffect(() => { useEffect(() => {
setMultiSelectionActive(multiSelectionKeyPressed); setMultiSelectionActive(multiSelectionKeyPressed);
+2
View File
@@ -21,6 +21,8 @@ export {
updateEdge, updateEdge,
getTransformForBounds, getTransformForBounds,
getRectOfNodes, getRectOfNodes,
applyNodeChanges,
applyEdgeChanges,
} from './utils/graph'; } from './utils/graph';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
+15 -2
View File
@@ -1,6 +1,8 @@
import { createAction } from './utils'; import { createAction } from './utils';
import { import {
Node,
Edge,
Elements, Elements,
OnConnectEndFunc, OnConnectEndFunc,
OnConnectFunc, OnConnectFunc,
@@ -18,6 +20,7 @@ import {
SnapGrid, SnapGrid,
ConnectionMode, ConnectionMode,
NodeExtent, NodeExtent,
OnElementsChange,
} from '../types'; } from '../types';
import * as constants from './contants'; import * as constants from './contants';
@@ -42,7 +45,8 @@ export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) =>
onConnectEnd, onConnectEnd,
}); });
export const setElements = (elements: Elements) => createAction(constants.SET_ELEMENTS, elements); export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes);
export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges);
export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) =>
createAction(constants.UPDATE_NODE_DIMENSIONS, updates); createAction(constants.UPDATE_NODE_DIMENSIONS, updates);
@@ -126,12 +130,19 @@ export const setConnectionMode = (connectionMode: ConnectionMode) =>
export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent); export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent);
export const setOnNodesChange = (onNodesChange: OnElementsChange) =>
createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange });
export const setOnEdgesChange = (onEdgesChange: OnElementsChange) =>
createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange });
export type ReactFlowAction = ReturnType< export type ReactFlowAction = ReturnType<
| typeof setOnConnect | typeof setOnConnect
| typeof setOnConnectStart | typeof setOnConnectStart
| typeof setOnConnectStop | typeof setOnConnectStop
| typeof setOnConnectEnd | typeof setOnConnectEnd
| typeof setElements | typeof setNodes
| typeof setEdges
| typeof updateNodeDimensions | typeof updateNodeDimensions
| typeof updateNodePos | typeof updateNodePos
| typeof updateNodePosDiff | typeof updateNodePosDiff
@@ -160,4 +171,6 @@ export type ReactFlowAction = ReturnType<
| typeof setMultiSelectionActive | typeof setMultiSelectionActive
| typeof setConnectionMode | typeof setConnectionMode
| typeof setNodeExtent | typeof setNodeExtent
| typeof setOnNodesChange
| typeof setOnEdgesChange
>; >;
+3 -2
View File
@@ -1,10 +1,11 @@
import { createStore, Store } from 'redux'; import { createStore, applyMiddleware, Store } from 'redux';
import thunk from 'redux-thunk';
import { ReactFlowState } from '../types'; import { ReactFlowState } from '../types';
import { ReactFlowAction } from './actions'; import { ReactFlowAction } from './actions';
import reactFlowReducer from './reducer'; import reactFlowReducer from './reducer';
export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> { export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> {
const store = createStore(reactFlowReducer, preloadedState); const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk));
return store; return store;
} }
+4 -1
View File
@@ -2,7 +2,8 @@ export const SET_ON_CONNECT = 'SET_ON_CONNECT';
export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START'; export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START';
export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP'; export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP';
export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END'; export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END';
export const SET_ELEMENTS = 'SET_ELEMENTS'; export const SET_NODES = 'SET_NODES';
export const SET_EDGES = 'SET_EDGES';
export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS'; export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS';
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF'; export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF';
@@ -31,3 +32,5 @@ export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE';
export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE'; export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE';
export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE'; export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE';
export const SET_NODE_EXTENT = 'SET_NODE_EXTENT'; export const SET_NODE_EXTENT = 'SET_NODE_EXTENT';
export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE';
export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE';
+3
View File
@@ -8,6 +8,9 @@ export const initialState: ReactFlowState = {
transform: [0, 0, 1], transform: [0, 0, 1],
nodes: [], nodes: [],
edges: [], edges: [],
onNodesChange: null,
onEdgesChange: null,
selectedElements: null, selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
+100 -91
View File
@@ -1,109 +1,111 @@
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { clampPosition, getDimensions } from '../utils'; import { clampPosition, getDimensions } from '../utils';
import { import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph';
getNodesInside,
getConnectedEdges,
getRectOfNodes,
isNode,
isEdge,
parseNode,
parseEdge,
} from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
import { getSourceTargetNodes } from '../container/EdgeRenderer/utils';
import { ReactFlowState, Node, XYPosition, Edge } from '../types'; import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types';
import * as constants from './contants'; import * as constants from './contants';
import { ReactFlowAction } from './actions'; import { ReactFlowAction } from './actions';
import { initialState } from './index'; import { initialState } from './index';
type NextElements = {
nextNodes: Node[];
nextEdges: Edge[];
};
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState { export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
switch (action.type) { switch (action.type) {
case constants.SET_ELEMENTS: { case constants.SET_NODES: {
const propElements = action.payload; const propNodes = action.payload;
const nextElements: NextElements = { const nextNodes = propNodes.map((propNode: Node) => {
nextNodes: [], const storeNode = state.nodes.find((node) => node.id === propNode.id);
nextEdges: [],
};
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = state.nodes.find((node) => node.id === propElement.id);
if (storeNode) { if (storeNode) {
if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) {
const updatedNode: Node = { const updatedNode: Node = {
...storeNode, ...storeNode,
...propElement, ...propNode,
}; };
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) { // When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.position = propElement.position; updatedNode.width = null;
} return updatedNode;
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.width = null;
}
res.nextNodes.push(updatedNode);
} else {
res.nextNodes.push(parseNode(propElement, state.nodeExtent));
}
} else if (isEdge(propElement)) {
const storeEdge = state.edges.find((se) => se.id === propElement.id);
if (storeEdge) {
res.nextEdges.push({
...storeEdge,
...propElement,
});
} else {
res.nextEdges.push(parseEdge(propElement));
} }
} }
return res; return parseNode(propNode, state.nodeExtent);
}, nextElements); });
return { ...state, nodes: nextNodes, edges: nextEdges }; const updatedEdges = state.edges.map((edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes);
if (sourceNode) {
edge.sourceNode = sourceNode;
}
if (targetNode) {
edge.targetNode = targetNode;
}
return edge;
});
return { ...state, nodes: nextNodes, edges: updatedEdges };
}
case constants.SET_EDGES: {
const propElements = action.payload;
const nextEdges = propElements.map((propEdge: Edge) => {
const storeEdge = state.edges.find((se) => se.id === propEdge.id);
if (storeEdge) {
return parseEdge(propEdge);
} else {
const parsedEdge = parseEdge(propEdge);
const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes);
if (sourceNode) {
parsedEdge.sourceNode = sourceNode;
}
if (targetNode) {
parsedEdge.targetNode = targetNode;
}
return parsedEdge;
}
});
return { ...state, edges: nextEdges };
} }
case constants.UPDATE_NODE_DIMENSIONS: { case constants.UPDATE_NODE_DIMENSIONS: {
const updatedNodes = state.nodes.map((node) => { const initialChanges: ElementChange[] = [];
const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => {
const update = action.payload.find((u) => u.id === node.id); const update = action.payload.find((u) => u.id === node.id);
if (update) { if (update) {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const doUpdate = const doUpdate =
dimensions.width && dimensions.width &&
dimensions.height && dimensions.height &&
(node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height || update.forceUpdate); (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate);
if (doUpdate) { if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]); const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
const change = {
return { id: node.id,
...node, change: {
__rf: {
...node.__rf,
...dimensions, ...dimensions,
handleBounds, handleBounds,
}, },
}; } as ElementChange;
res.push(change);
} }
} }
return node; return res;
}); }, initialChanges);
return { if (state.onNodesChange) {
...state, requestAnimationFrame(() => state.onNodesChange?.(nodesToChange));
nodes: updatedNodes, }
};
return state;
} }
case constants.UPDATE_NODE_POS: { case constants.UPDATE_NODE_POS: {
const { id, pos } = action.payload; const { id, pos } = action.payload;
@@ -117,13 +119,20 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
}; };
} }
if (state.onNodesChange) {
state.onNodesChange([{ id, change: { position } }]);
return state;
}
const nextNodes = state.nodes.map((node) => { const nextNodes = state.nodes.map((node) => {
if (node.id === id) { if (node.id === id) {
return { return {
...node, ...node,
position,
__rf: { __rf: {
...node.__rf, ...node.__rf,
position,
}, },
}; };
} }
@@ -136,30 +145,28 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
case constants.UPDATE_NODE_POS_DIFF: { case constants.UPDATE_NODE_POS_DIFF: {
const { id, diff, isDragging } = action.payload; const { id, diff, isDragging } = action.payload;
const nextNodes = state.nodes.map((node) => { if (state.onNodesChange && id && diff) {
if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) { const matchingNode = state.nodes.find((n) => n.id === id);
const updatedNode = {
...node,
__rf: {
...node.__rf,
isDragging,
},
};
if (diff) { if (matchingNode) {
updatedNode.__rf.position = { requestAnimationFrame(() =>
x: node.__rf.position.x + diff.x, state.onNodesChange?.([
y: node.__rf.position.y + diff.y, {
}; id,
} change: {
position: {
return updatedNode; x: matchingNode.position.x + diff.x,
y: matchingNode.position.y + diff.y,
isDragging,
},
},
},
])
);
} }
}
return node; return state;
});
return { ...state, nodes: nextNodes };
} }
case constants.SET_USER_SELECTION: { case constants.SET_USER_SELECTION: {
const mousePos = action.payload; const mousePos = action.payload;
@@ -308,9 +315,9 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
nodes: state.nodes.map((node) => { nodes: state.nodes.map((node) => {
return { return {
...node, ...node,
position: clampPosition(node.position, nodeExtent),
__rf: { __rf: {
...node.__rf, ...node.__rf,
position: clampPosition(node.__rf.position, nodeExtent),
}, },
}; };
}), }),
@@ -334,6 +341,8 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
case constants.SET_ELEMENTS_SELECTABLE: case constants.SET_ELEMENTS_SELECTABLE:
case constants.SET_MULTI_SELECTION_ACTIVE: case constants.SET_MULTI_SELECTION_ACTIVE:
case constants.SET_CONNECTION_MODE: case constants.SET_CONNECTION_MODE:
case constants.SET_ON_NODES_CHANGE:
case constants.SET_ON_EDGES_CHANGE:
return { ...state, ...action.payload }; return { ...state, ...action.payload };
default: default:
return state; return state;
+17 -1
View File
@@ -9,6 +9,12 @@ export type Elements<T = any> = Array<FlowElement<T>>;
export type Transform = [number, number, number]; export type Transform = [number, number, number];
export type ElementChange = {
id: string;
change?: any;
delete?: boolean;
};
export enum Position { export enum Position {
Left = 'left', Left = 'left',
Top = 'top', Top = 'top',
@@ -50,6 +56,10 @@ export interface Node<T = any> {
selectable?: boolean; selectable?: boolean;
connectable?: boolean; connectable?: boolean;
dragHandle?: string; dragHandle?: string;
isDragging?: boolean;
width?: number | null;
height?: number | null;
handleBounds?: any;
} }
export enum ArrowHeadType { export enum ArrowHeadType {
@@ -76,6 +86,8 @@ export interface Edge<T = any> {
isHidden?: boolean; isHidden?: boolean;
data?: T; data?: T;
className?: string; className?: string;
sourceNode?: Node;
targetNode?: Node;
} }
export enum BackgroundVariant { export enum BackgroundVariant {
@@ -313,7 +325,7 @@ export type ConnectionLineComponentProps = {
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>;
export type OnConnectFunc = (connection: Connection) => void; export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void;
export type OnConnectStartParams = { export type OnConnectStartParams = {
nodeId: ElementId | null; nodeId: ElementId | null;
handleId: ElementId | null; handleId: ElementId | null;
@@ -398,6 +410,8 @@ export type InitD3ZoomPayload = {
transform: Transform; transform: Transform;
}; };
export type OnElementsChange = (nodes: ElementChange[]) => void;
export interface ReactFlowState { export interface ReactFlowState {
width: number; width: number;
height: number; height: number;
@@ -406,6 +420,8 @@ export interface ReactFlowState {
edges: Edge[]; edges: Edge[];
selectedElements: Elements | null; selectedElements: Elements | null;
selectedNodesBbox: Rect; selectedNodesBbox: Rect;
onNodesChange: OnElementsChange | null;
onEdgesChange: OnElementsChange | null;
d3Zoom: ZoomBehavior<Element, unknown> | null; d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null; d3Selection: D3Selection<Element, unknown, null, undefined> | null;
+54 -42
View File
@@ -15,6 +15,7 @@ import {
FlowExportObject, FlowExportObject,
ReactFlowState, ReactFlowState,
NodeExtent, NodeExtent,
ElementChange,
} from '../types'; } from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge => export const isEdge = (element: Node | Connection | Edge): element is Edge =>
@@ -68,10 +69,10 @@ const connectionExists = (edge: Edge, elements: Elements) => {
); );
}; };
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => { export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
console.warn("Can't create edge. An edge needs a source and a target."); console.warn("Can't create edge. An edge needs a source and a target.");
return elements; return edges;
} }
let edge: Edge; let edge: Edge;
@@ -84,11 +85,11 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
} as Edge; } as Edge;
} }
if (connectionExists(edge, elements)) { if (connectionExists(edge, nodes)) {
return elements; return edges;
} }
return elements.concat(edge); return edges.concat(edge);
}; };
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => { export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => {
@@ -147,30 +148,23 @@ export const onLoadProject = (currentStore: Store<ReactFlowState>) => {
}; };
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => { export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
return { if (!node.type) {
...node, node.type = 'default';
id: node.id.toString(), }
type: node.type || 'default',
__rf: { if (nodeExtent) {
position: clampPosition(node.position, nodeExtent), node.position = clampPosition(node.position, nodeExtent);
width: null, }
height: null,
handleBounds: {}, return node;
isDragging: false,
},
};
}; };
export const parseEdge = (edge: Edge): Edge => { export const parseEdge = (edge: Edge): Edge => {
return { if (!edge.type) {
...edge, edge.type = 'default';
source: edge.source.toString(), }
target: edge.target.toString(),
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null, return edge;
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
id: edge.id.toString(),
type: edge.type || 'default',
};
}; };
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
@@ -199,8 +193,8 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
export const getRectOfNodes = (nodes: Node[]): Rect => { export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce( const box = nodes.reduce(
(currBox, { __rf: { position, width, height } = {} }) => (currBox, { position, width, height }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })), getBoundsOfBoxes(currBox, rectToBox({ ...position, width: width || 0, height: height || 0 })),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
); );
@@ -227,12 +221,12 @@ export const getNodesInside = (
height: rect.height / tScale, height: rect.height / tScale,
}); });
return nodes.filter(({ selectable = true, __rf: { position, width, height, isDragging } }) => { return nodes.filter(({ selectable = true, position, width, height, isDragging }) => {
if (excludeNonSelectableNodes && !selectable) { if (excludeNonSelectableNodes && !selectable) {
return false; return false;
} }
const nBox = rectToBox({ ...position, width, height }); const nBox = rectToBox({ ...position, width: width || 0, height: height || 0 });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap); const overlappingArea = Math.ceil(xOverlap * yOverlap);
@@ -246,7 +240,7 @@ export const getNodesInside = (
return overlappingArea > 0; return overlappingArea > 0;
} }
const area = width * height; const area = (width || 0) * (height || 0);
return overlappingArea >= area; return overlappingArea >= area;
}); });
@@ -259,17 +253,7 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
}; };
const parseElements = (nodes: Node[], edges: Edge[]): Elements => { const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
return [ return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))];
...nodes.map((node) => {
const n = { ...node };
n.position = n.__rf.position;
delete n.__rf;
return n;
}),
...edges.map((e) => ({ ...e })),
];
}; };
export const onLoadGetElements = (currentStore: Store<ReactFlowState>) => { export const onLoadGetElements = (currentStore: Store<ReactFlowState>) => {
@@ -311,3 +295,31 @@ export const getTransformForBounds = (
return [x, y, clampedZoom]; return [x, y, clampedZoom];
}; };
function applyChanges(changes: ElementChange[], elements: any[]): any[] {
const initElements: any[] = [];
return elements.reduce((res: any[], node: any) => {
const hasChange = changes.find((c) => c.id === node.id);
if (hasChange?.delete) {
return res;
}
if (hasChange?.change) {
res.push({ ...node, ...hasChange.change });
} else {
res.push(node);
}
return res;
}, initElements);
}
export function applyNodeChanges(changes: ElementChange[], nodes: Node[]): Node[] {
return applyChanges(changes, nodes) as Node[];
}
export function applyEdgeChanges(changes: ElementChange[], edges: Edge[]): Edge[] {
return applyChanges(changes, edges) as Edge[];
}