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

View File

@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth,
className,
borderRadius,
shapeRendering
shapeRendering,
}: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string;

View File

@@ -41,9 +41,9 @@ const MiniMap = ({
const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function
? nodeStrokeColor
: () => nodeStrokeColor) as StringFunc;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
const hasNodes = nodes && nodes.length;
const bb = getRectOfNodes(nodes);
@@ -64,7 +64,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + 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 (
<svg
@@ -75,14 +75,14 @@ const MiniMap = ({
className={mapClasses}
>
{nodes
.filter((node) => !node.isHidden)
.filter((node) => !node.isHidden && node.width && node.height)
.map((node) => (
<MiniMapNode
key={node.id}
x={node.__rf.position.x}
y={node.__rf.position.y}
width={node.__rf.width}
height={node.__rf.height}
x={node.position.x}
y={node.position.y}
width={node.width!}
height={node.height!}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState, CSSProperties } from 'react';
import { useStoreState } from '../../store/hooks';
import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import {
@@ -20,7 +21,6 @@ interface ConnectionLineProps {
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
@@ -35,11 +35,11 @@ export default ({
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform,
isConnectable,
CustomConnectionLineComponent,
}: ConnectionLineProps) => {
const nodes = useStoreState((state) => state.nodes);
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId;
const handleId = connectionHandleId;
@@ -54,12 +54,12 @@ export default ({
}
const sourceHandle = handleId
? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
: sourceNode.__rf.handleBounds[connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
: sourceNode.handleBounds[connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height;
const sourceX = sourceNode.position.x + sourceHandleX;
const sourceY = sourceNode.position.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2];

View File

@@ -1,18 +1,24 @@
import { useEffect } from 'react';
import { useStoreActions } from '../../store/hooks';
import { Elements } from '../../types';
import { Node, Edge } from '../../types';
interface ElementUpdaterProps {
elements: Elements;
nodes: Node[];
edges: Edge[];
}
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
const setElements = useStoreActions((actions) => actions.setElements);
const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => {
const setNodes = useStoreActions((actions) => actions.setNodes);
const setEdges = useStoreActions((actions) => actions.setEdges);
useEffect(() => {
setElements(elements);
}, [elements]);
setNodes(nodes);
}, [nodes]);
useEffect(() => {
setEdges(edges);
}, [edges]);
return null;
};

View File

@@ -1,6 +1,9 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { Store } from 'redux';
import { getHostForElement } from '../../utils';
import { ReactFlowState } from '../../types';
import { ReactFlowAction } from '../../store/actions';
import {
ElementId,
@@ -103,7 +106,8 @@ export function onMouseDown(
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc
onConnectEnd?: OnConnectEndFunc,
store?: Store<ReactFlowState, ReactFlowAction>
): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
@@ -177,7 +181,7 @@ export function onMouseDown(
onConnectStop?.(event);
if (isValid) {
onConnect?.(connection);
onConnect?.(connection, store?.getState().nodes || []);
}
onConnectEnd?.(event);

View File

@@ -1,9 +1,9 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { useStoreActions, useStoreState, useStore } from '../../store/hooks';
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';
@@ -26,6 +26,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
},
ref
) => {
const store = useStore();
const nodeId = useContext(NodeIdContext) as ElementId;
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
@@ -38,9 +39,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const isTarget = type === 'target';
const onConnectExtended = useCallback(
(params: Connection) => {
onConnectAction?.(params);
onConnect?.(params);
(params: Connection, nodes: Node[]) => {
onConnectAction?.(params, nodes);
onConnect?.(params, nodes);
},
[onConnectAction, onConnect]
);
@@ -61,7 +62,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
onConnectEnd,
store
);
},
[

View File

@@ -1,18 +1,8 @@
import React, {
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat';
import { useStoreActions } from '../../store/hooks';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types';
@@ -50,9 +40,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
resizeObserver,
dragHandle,
}: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
// const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
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 nodeElement = useRef<HTMLDivElement>(null);
@@ -84,6 +74,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onMouseLeave,
]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
return;
@@ -153,20 +144,25 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosDiff({
id,
diff: {
x: draggableData.deltaX,
y: draggableData.deltaY,
onNodesChange?.([
{
id,
change: {
position: {
x: node.position.x,
y: node.position.y,
},
isDragging: true,
},
},
isDragging: true,
});
]);
},
[id, node, onNodeDrag]
);
@@ -185,10 +181,14 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
return;
}
updateNodePosDiff({
id: node.id,
isDragging: false,
});
onNodesChange?.([
{
id: node.id,
change: {
isDragging: true,
},
},
]);
onNodeDragStop?.(event as MouseEvent, node);
},
@@ -202,11 +202,11 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
[node, onNodeDoubleClick]
);
useLayoutEffect(() => {
if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition]);
// useEffect(() => {
// if (nodeElement.current && !isHidden) {
// updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
// }
// }, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => {
if (nodeElement.current) {

View File

@@ -45,7 +45,7 @@ export default ({
return {
...matchingNode,
position: matchingNode?.__rf.position,
position: matchingNode?.position,
} as Node;
})
: [],

View File

@@ -4,7 +4,7 @@ import { useStoreState } from '../../store/hooks';
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 } from './utils';
import {
Position,
Edge,
@@ -14,7 +14,6 @@ import {
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
Transform,
OnEdgeUpdateFunc,
} from '../../types';
@@ -41,151 +40,167 @@ interface EdgeRendererProps {
interface EdgeWrapperProps {
edge: Edge;
props: EdgeRendererProps;
nodes: Node[];
edgeTypes: any;
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;
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,
const Edge = memo(
({
edge,
edgeTypes,
markerEndId,
onElementClick,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
edgeUpdaterRadius,
onEdgeDoubleClick,
onEdgeUpdateStart,
onEdgeUpdateEnd,
onEdgeUpdate,
targetNode,
targetHandle,
targetPosition
);
sourceNode,
selectedElements,
elementsSelectable,
connectionMode,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const isVisible = onlyRenderVisibleElements
? isEdgeVisible({
sourcePos: { x: sourceX, y: sourceY },
targetPos: { x: targetX, y: targetY },
width,
height,
transform,
})
: true;
const onConnectEdge = useCallback(
(connection: Connection) => {
onEdgeUpdate?.(edge, connection);
},
[edge, onEdgeUpdate]
);
if (!isVisible) {
return null;
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.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 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);
@@ -201,13 +216,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
return null;
}
const {
connectionLineType,
arrowHeadColor,
connectionLineStyle,
connectionLineComponent,
onlyRenderVisibleElements,
} = props;
const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props;
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
const renderConnectionLine = connectionNodeId && connectionHandleType;
@@ -219,19 +228,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<Edge
key={edge.id}
edge={edge}
props={props}
nodes={nodes}
sourceNode={edge.sourceNode}
targetNode={edge.targetNode}
selectedElements={selectedElements}
elementsSelectable={elementsSelectable}
transform={transform}
width={width}
height={height}
onlyRenderVisibleElements={onlyRenderVisibleElements}
markerEndId={props.markerEndId}
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}
/>
))}
{renderConnectionLine && (
<ConnectionLine
nodes={nodes}
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}

View File

@@ -40,8 +40,8 @@ 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 x = (handle?.x || 0) + node.position.x;
const y = (handle?.y || 0) + node.position.y;
const width = handle?.width || node.__rf.width;
const height = handle?.height || node.__rf.height;

View File

@@ -30,7 +30,6 @@ const FlowRenderer = ({
onPaneClick,
onPaneContextMenu,
onPaneScroll,
onElementsRemove,
deleteKeyCode,
onMove,
onMoveStart,
@@ -61,7 +60,7 @@ const FlowRenderer = ({
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode });
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = useCallback(
(event: MouseEvent) => {

View File

@@ -11,7 +11,7 @@ import { ReactFlowProps } from '../ReactFlow';
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;
edgeTypes: EdgeTypesType;
selectionKeyCode: KeyCode;
@@ -55,7 +55,6 @@ const GraphView = ({
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
onElementsRemove,
deleteKeyCode,
onConnect,
onConnectStart,
@@ -95,6 +94,8 @@ const GraphView = ({
edgeUpdaterRadius,
onEdgeUpdateStart,
onEdgeUpdateEnd,
onNodesChange,
onEdgesChange,
}: GraphViewProps) => {
const isInitialized = useRef<boolean>(false);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
@@ -111,6 +112,9 @@ const GraphView = ({
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange);
const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange);
const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
@@ -217,12 +221,23 @@ const GraphView = ({
}
}, [connectionMode]);
useEffect(() => {
if (typeof onNodesChange !== 'undefined') {
setOnNodesChange(onNodesChange);
}
}, [onNodesChange]);
useEffect(() => {
if (typeof onEdgesChange !== 'undefined') {
setOnEdgesChange(onEdgesChange);
}
}, [onEdgesChange]);
return (
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}

View File

@@ -1,6 +1,5 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
import { getNodesInside } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
interface NodeRendererProps {
@@ -26,14 +25,12 @@ const NodeRenderer = (props: NodeRendererProps) => {
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
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 nodes = useStoreState((state) => state.nodes);
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const visibleNodes = props.onlyRenderVisibleElements
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes;
// const visibleNodes = props.onlyRenderVisibleElements
// ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
// : nodes;
const transformStyle = useMemo(
() => ({
@@ -59,7 +56,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
return (
<div className="react-flow__nodes" style={transformStyle}>
{visibleNodes.map((node) => {
{nodes.map((node) => {
const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
@@ -82,10 +79,10 @@ const NodeRenderer = (props: NodeRendererProps) => {
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
isHidden={node.isHidden}
xPos={node.__rf.position.x}
yPos={node.__rf.position.y}
isDragging={node.__rf.isDragging}
isInitialized={node.__rf.width !== null && node.__rf.height !== null}
xPos={node.position.x}
yPos={node.position.y}
isDragging={node.isDragging}
isInitialized={node.width !== null && node.height !== null}
snapGrid={props.snapGrid}
snapToGrid={props.snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}

View File

@@ -25,7 +25,6 @@ import {
OnLoadFunc,
Node,
Edge,
Connection,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
@@ -33,11 +32,13 @@ import {
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
OnConnectFunc,
TranslateExtent,
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
NodeExtent,
ElementChange,
} from '../../types';
import '../../style.css';
@@ -57,9 +58,11 @@ const defaultEdgeTypes = {
};
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;
onElementsRemove?: (elements: Elements) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (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;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onConnect?: (connection: Edge | Connection) => void;
onConnect?: OnConnectFunc;
onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc;
@@ -132,10 +135,14 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
export type ReactFlowRefType = HTMLDivElement;
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
(
{
elements = [],
nodes = [],
edges = [],
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
@@ -144,7 +151,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onMove,
onMoveStart,
onMoveEnd,
onElementsRemove,
onConnect,
onConnectStart,
onConnectStop,
@@ -171,7 +177,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta',
snapToGrid = false,
snapGrid = [15, 15],
snapGrid = initSnapGrid,
onlyRenderVisibleElements = false,
selectNodesOnDrag = true,
nodesDraggable,
@@ -180,7 +186,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
minZoom,
maxZoom,
defaultZoom = 1,
defaultPosition = [0, 0],
defaultPosition = initDefaultPosition,
translateExtent,
preventScrolling = true,
nodeExtent,
@@ -208,6 +214,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
edgeUpdaterRadius = 10,
nodeTypesId = '1',
edgeTypesId = '1',
onNodesChange,
onEdgesChange,
...rest
},
ref
@@ -240,7 +248,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
selectionKeyCode={selectionKeyCode}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
@@ -287,8 +294,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>
<ElementUpdater elements={elements} />
<ElementUpdater nodes={nodes} edges={edges} />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
</Wrapper>

View File

@@ -1,22 +1,23 @@
import { useEffect } from 'react';
import { useStore, useStoreActions } from '../store/hooks';
import { useStore, useStoreActions, useStoreState } from '../store/hooks';
import useKeyPress from './useKeyPress';
import { isNode, getConnectedEdges } from '../utils/graph';
import { Elements, KeyCode, ElementId, FlowElement } from '../types';
import { isNode, isEdge, getConnectedEdges } from '../utils/graph';
import { KeyCode } from '../types';
interface HookParams {
deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode;
onElementsRemove?: (elements: Elements) => void;
}
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStore();
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
const onNodesChange = useStoreState((state) => state.onNodesChange);
const onEdgesChange = useStoreState((state) => state.onEdgesChange);
const deleteKeyPressed = useKeyPress(deleteKeyCode);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
@@ -24,19 +25,21 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
useEffect(() => {
const { edges, selectedElements } = store.getState();
if (onElementsRemove && deleteKeyPressed && selectedElements) {
if (deleteKeyPressed && selectedElements) {
const selectedNodes = selectedElements.filter(isNode);
const selectedEdges = selectedElements.filter(isEdge);
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();
resetSelectedElements();
}
}, [deleteKeyPressed, onElementsRemove]);
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
useEffect(() => {
setMultiSelectionActive(multiSelectionKeyPressed);

View File

@@ -21,6 +21,8 @@ export {
updateEdge,
getTransformForBounds,
getRectOfNodes,
applyNodeChanges,
applyEdgeChanges,
} from './utils/graph';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';

View File

@@ -1,6 +1,8 @@
import { createAction } from './utils';
import {
Node,
Edge,
Elements,
OnConnectEndFunc,
OnConnectFunc,
@@ -18,6 +20,7 @@ import {
SnapGrid,
ConnectionMode,
NodeExtent,
OnElementsChange,
} from '../types';
import * as constants from './contants';
@@ -42,7 +45,8 @@ export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) =>
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[]) =>
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 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<
| typeof setOnConnect
| typeof setOnConnectStart
| typeof setOnConnectStop
| typeof setOnConnectEnd
| typeof setElements
| typeof setNodes
| typeof setEdges
| typeof updateNodeDimensions
| typeof updateNodePos
| typeof updateNodePosDiff
@@ -160,4 +171,6 @@ export type ReactFlowAction = ReturnType<
| typeof setMultiSelectionActive
| typeof setConnectionMode
| typeof setNodeExtent
| typeof setOnNodesChange
| typeof setOnEdgesChange
>;

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 { ReactFlowAction } from './actions';
import reactFlowReducer from './reducer';
export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> {
const store = createStore(reactFlowReducer, preloadedState);
const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk));
return store;
}

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_STOP = 'SET_ON_CONNECT_STOP';
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_POS = 'UPDATE_NODE_POS';
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_CONNECTION_MODE = 'SET_CONNECTION_MODE';
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';

View File

@@ -8,6 +8,9 @@ export const initialState: ReactFlowState = {
transform: [0, 0, 1],
nodes: [],
edges: [],
onNodesChange: null,
onEdgesChange: null,
selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },

View File

@@ -1,109 +1,111 @@
import isEqual from 'fast-deep-equal';
import { clampPosition, getDimensions } from '../utils';
import {
getNodesInside,
getConnectedEdges,
getRectOfNodes,
isNode,
isEdge,
parseNode,
parseEdge,
} from '../utils/graph';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph';
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 { ReactFlowAction } from './actions';
import { initialState } from './index';
type NextElements = {
nextNodes: Node[];
nextEdges: Edge[];
};
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
switch (action.type) {
case constants.SET_ELEMENTS: {
const propElements = action.payload;
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
};
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = state.nodes.find((node) => node.id === propElement.id);
case constants.SET_NODES: {
const propNodes = action.payload;
const nextNodes = propNodes.map((propNode: Node) => {
const storeNode = state.nodes.find((node) => node.id === propNode.id);
if (storeNode) {
if (storeNode) {
if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) {
const updatedNode: Node = {
...storeNode,
...propElement,
...propNode,
};
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
updatedNode.__rf.position = propElement.position;
}
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));
// 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.width = null;
return updatedNode;
}
}
return res;
}, nextElements);
return parseNode(propNode, state.nodeExtent);
});
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: {
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);
if (update) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate =
dimensions.width &&
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) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
return {
...node,
__rf: {
...node.__rf,
const change = {
id: node.id,
change: {
...dimensions,
handleBounds,
},
};
} as ElementChange;
res.push(change);
}
}
return node;
});
return res;
}, initialChanges);
return {
...state,
nodes: updatedNodes,
};
if (state.onNodesChange) {
requestAnimationFrame(() => state.onNodesChange?.(nodesToChange));
}
return state;
}
case constants.UPDATE_NODE_POS: {
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) => {
if (node.id === id) {
return {
...node,
position,
__rf: {
...node.__rf,
position,
},
};
}
@@ -136,30 +145,28 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
case constants.UPDATE_NODE_POS_DIFF: {
const { id, diff, isDragging } = action.payload;
const nextNodes = state.nodes.map((node) => {
if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) {
const updatedNode = {
...node,
__rf: {
...node.__rf,
isDragging,
},
};
if (state.onNodesChange && id && diff) {
const matchingNode = state.nodes.find((n) => n.id === id);
if (diff) {
updatedNode.__rf.position = {
x: node.__rf.position.x + diff.x,
y: node.__rf.position.y + diff.y,
};
}
return updatedNode;
if (matchingNode) {
requestAnimationFrame(() =>
state.onNodesChange?.([
{
id,
change: {
position: {
x: matchingNode.position.x + diff.x,
y: matchingNode.position.y + diff.y,
isDragging,
},
},
},
])
);
}
}
return node;
});
return { ...state, nodes: nextNodes };
return state;
}
case constants.SET_USER_SELECTION: {
const mousePos = action.payload;
@@ -308,9 +315,9 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow
nodes: state.nodes.map((node) => {
return {
...node,
position: clampPosition(node.position, nodeExtent),
__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_MULTI_SELECTION_ACTIVE:
case constants.SET_CONNECTION_MODE:
case constants.SET_ON_NODES_CHANGE:
case constants.SET_ON_EDGES_CHANGE:
return { ...state, ...action.payload };
default:
return state;

View File

@@ -9,6 +9,12 @@ export type Elements<T = any> = Array<FlowElement<T>>;
export type Transform = [number, number, number];
export type ElementChange = {
id: string;
change?: any;
delete?: boolean;
};
export enum Position {
Left = 'left',
Top = 'top',
@@ -50,6 +56,10 @@ export interface Node<T = any> {
selectable?: boolean;
connectable?: boolean;
dragHandle?: string;
isDragging?: boolean;
width?: number | null;
height?: number | null;
handleBounds?: any;
}
export enum ArrowHeadType {
@@ -76,6 +86,8 @@ export interface Edge<T = any> {
isHidden?: boolean;
data?: T;
className?: string;
sourceNode?: Node;
targetNode?: Node;
}
export enum BackgroundVariant {
@@ -313,7 +325,7 @@ export type ConnectionLineComponentProps = {
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>;
export type OnConnectFunc = (connection: Connection) => void;
export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void;
export type OnConnectStartParams = {
nodeId: ElementId | null;
handleId: ElementId | null;
@@ -398,6 +410,8 @@ export type InitD3ZoomPayload = {
transform: Transform;
};
export type OnElementsChange = (nodes: ElementChange[]) => void;
export interface ReactFlowState {
width: number;
height: number;
@@ -406,6 +420,8 @@ export interface ReactFlowState {
edges: Edge[];
selectedElements: Elements | null;
selectedNodesBbox: Rect;
onNodesChange: OnElementsChange | null;
onEdgesChange: OnElementsChange | null;
d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null;

View File

@@ -15,6 +15,7 @@ import {
FlowExportObject,
ReactFlowState,
NodeExtent,
ElementChange,
} from '../types';
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) {
console.warn("Can't create edge. An edge needs a source and a target.");
return elements;
return edges;
}
let edge: Edge;
@@ -84,11 +85,11 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
} as Edge;
}
if (connectionExists(edge, elements)) {
return elements;
if (connectionExists(edge, nodes)) {
return edges;
}
return elements.concat(edge);
return edges.concat(edge);
};
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 => {
return {
...node,
id: node.id.toString(),
type: node.type || 'default',
__rf: {
position: clampPosition(node.position, nodeExtent),
width: null,
height: null,
handleBounds: {},
isDragging: false,
},
};
if (!node.type) {
node.type = 'default';
}
if (nodeExtent) {
node.position = clampPosition(node.position, nodeExtent);
}
return node;
};
export const parseEdge = (edge: Edge): Edge => {
return {
...edge,
source: edge.source.toString(),
target: edge.target.toString(),
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
id: edge.id.toString(),
type: edge.type || 'default',
};
if (!edge.type) {
edge.type = 'default';
}
return edge;
};
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 => {
const box = nodes.reduce(
(currBox, { __rf: { position, width, height } = {} }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })),
(currBox, { position, width, height }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width: width || 0, height: height || 0 })),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
);
@@ -227,12 +221,12 @@ export const getNodesInside = (
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) {
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 yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
@@ -246,7 +240,7 @@ export const getNodesInside = (
return overlappingArea > 0;
}
const area = width * height;
const area = (width || 0) * (height || 0);
return overlappingArea >= area;
});
@@ -259,17 +253,7 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
};
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
return [
...nodes.map((node) => {
const n = { ...node };
n.position = n.__rf.position;
delete n.__rf;
return n;
}),
...edges.map((e) => ({ ...e })),
];
return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))];
};
export const onLoadGetElements = (currentStore: Store<ReactFlowState>) => {
@@ -311,3 +295,31 @@ export const getTransformForBounds = (
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[];
}