Merge branch 'main' into v11

This commit is contained in:
moklick
2022-07-18 18:08:00 +02:00
39 changed files with 7344 additions and 5892 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ dist
stats.html stats.html
.eslintcache .eslintcache
.idea .idea
.yarn

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -10,7 +10,7 @@
"react-flow-renderer": "file:../", "react-flow-renderer": "file:../",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.6.4" "typescript": "^4.7.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -9,6 +9,7 @@ import ReactFlow, {
useReactFlow, useReactFlow,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
@@ -61,6 +62,7 @@ const BasicFlow = () => {
defaultEdges={initialEdges} defaultEdges={initialEdges}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example" className="react-flow-basic-example"
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'react-flow-renderer'; import { ConnectionLineComponentProps } from 'react-flow-renderer';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ sourceX, sourceY, targetX, targetY }) => { const ConnectionLine: FC<ConnectionLineComponentProps> = ({ sourceX, sourceY, targetX, targetY, toX, toY }) => {
return ( return (
<g> <g>
<path <path
@@ -11,7 +11,7 @@ const ConnectionLine: FC<ConnectionLineComponentProps> = ({ sourceX, sourceY, ta
className="animated" className="animated"
d={`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`} d={`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`}
/> />
<circle cx={targetX} cy={targetY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} /> <circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g> </g>
); );
}; };

View File

@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
Node, Node,
addEdge, addEdge,
@@ -11,13 +12,13 @@ import ReactFlow, {
import ConnectionLine from './ConnectionLine'; import ConnectionLine from './ConnectionLine';
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }]; const initialNodes: Node[] = [{ id: '1', type: 'default', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
const initialEdges: Edge[] = []; const initialEdges: Edge[] = [];
const ConnectionLineFlow = () => { const ConnectionLineFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return ( return (
<ReactFlow <ReactFlow

View File

@@ -1,5 +1,5 @@
import React, { memo, useCallback, Dispatch, FC } from 'react'; import React, { memo, useCallback, Dispatch, FC } from 'react';
import { useReactFlow, ReactFlowInstance, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer'; import { useReactFlow, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer';
import localforage from 'localforage'; import localforage from 'localforage';
localforage.config({ localforage.config({
@@ -12,20 +12,17 @@ const flowKey = 'example-flow';
const getNodeId = () => `randomnode_${+new Date()}`; const getNodeId = () => `randomnode_${+new Date()}`;
type ControlsProps = { type ControlsProps = {
rfInstance?: ReactFlowInstance;
setNodes: Dispatch<React.SetStateAction<Node<any>[]>>; setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>; setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
}; };
const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => { const Controls: FC<ControlsProps> = ({ setNodes, setEdges }) => {
const { setViewport } = useReactFlow(); const { setViewport, toObject } = useReactFlow();
const onSave = useCallback(() => { const onSave = useCallback(() => {
if (rfInstance) { const flow = toObject();
const flow = rfInstance.toObject(); localforage.setItem(flowKey, flow);
localforage.setItem(flowKey, flow); }, [toObject]);
}
}, [rfInstance]);
const onRestore = useCallback(() => { const onRestore = useCallback(() => {
const restoreFlow = async () => { const restoreFlow = async () => {
@@ -33,6 +30,7 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
if (flow) { if (flow) {
const { x, y, zoom } = flow.viewport; const { x, y, zoom } = flow.viewport;
setNodes(flow.nodes || []); setNodes(flow.nodes || []);
setEdges(flow.edges || []); setEdges(flow.edges || []);
setViewport({ x, y, zoom: zoom || 0 }); setViewport({ x, y, zoom: zoom || 0 });

View File

@@ -1,11 +1,10 @@
import { useState } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
Node, Node,
addEdge, addEdge,
Connection, Connection,
Edge, Edge,
ReactFlowInstance,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
@@ -22,10 +21,9 @@ const initialNodes: Node[] = [
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const SaveRestore = () => { const SaveRestore = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)); const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
@@ -35,9 +33,8 @@ const SaveRestore = () => {
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
onInit={setRfInstance}
> >
<Controls rfInstance={rfInstance} setNodes={setNodes} setEdges={setEdges} /> <Controls setNodes={setNodes} setEdges={setEdges} />
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
); );

View File

@@ -11,7 +11,7 @@ const idStyle: CSSProperties = {
left: 2, left: 2,
}; };
const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -24,4 +24,4 @@ const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
); );
}; };
export default memo(ColorSelectorNode); export default memo(DebugNode);

View File

@@ -15,6 +15,7 @@ import ReactFlow, {
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
const onNodeDrag = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes); const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
@@ -82,6 +83,7 @@ const initialNodes: Node[] = [
position: { x: 25, y: 50 }, position: { x: 25, y: 50 },
className: 'light', className: 'light',
parentNode: '5', parentNode: '5',
extent: 'parent',
}, },
{ {
id: '5b', id: '5b',
@@ -89,6 +91,7 @@ const initialNodes: Node[] = [
position: { x: 225, y: 50 }, position: { x: 225, y: 50 },
className: 'light', className: 'light',
parentNode: '5', parentNode: '5',
expandParent: true,
}, },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
@@ -167,6 +170,7 @@ const Subflow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example" className="react-flow-basic-example"
defaultZoom={1.5} defaultZoom={1.5}

View File

@@ -1,4 +1,4 @@
import { MouseEvent as ReactMouseEvent, FC } from 'react'; import { FC, useCallback, useState } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Handle, Handle,
@@ -6,11 +6,11 @@ import ReactFlow, {
Position, Position,
Node, Node,
Edge, Edge,
OnConnectStartParams,
NodeProps, NodeProps,
NodeTypes, NodeTypes,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
OnConnectStartParams,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import './validation.css'; import './validation.css';
@@ -23,10 +23,6 @@ const initialNodes: Node[] = [
]; ];
const isValidConnection = (connection: Connection) => connection.target === 'B'; const isValidConnection = (connection: Connection) => connection.target === 'B';
const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) =>
console.log('on connect start', { nodeId, handleType });
const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event);
const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event);
const CustomInput: FC<NodeProps> = () => ( const CustomInput: FC<NodeProps> = () => (
<> <>
@@ -49,13 +45,33 @@ const nodeTypes: NodeTypes = {
}; };
const ValidationFlow = () => { const ValidationFlow = () => {
const [value, setValue] = useState(0);
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => { const onConnectStart = useCallback(
console.log('on connect', params); (event: React.MouseEvent, params: OnConnectStartParams) => {
setEdges((eds) => addEdge(params, eds)); console.log('on connect start', params, event, value);
}; setValue(1);
},
[value]
);
const onConnect = useCallback(
(params: Connection | Edge) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
const onConnectEnd = useCallback(
(event: MouseEvent) => {
console.log('on connect end', event, value);
setValue(0);
},
[value]
);
return ( return (
<ReactFlow <ReactFlow
@@ -68,7 +84,6 @@ const ValidationFlow = () => {
className="validationflow" className="validationflow"
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
fitView fitView
/> />

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph'; import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils'; import { getBoundsofRects } from '../../utils';
import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types'; import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState } from '../../types';
declare const window: any; declare const window: any;
@@ -15,9 +15,12 @@ const defaultWidth = 200;
const defaultHeight = 150; const defaultHeight = 150;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
width: s.width, viewBBox: {
height: s.height, x: -s.transform[0] / s.transform[2],
transform: s.transform, y: -s.transform[1] / s.transform[2],
width: s.width / s.transform[2],
height: s.height / s.transform[2],
},
nodes: Array.from(s.nodeInternals.values()), nodes: Array.from(s.nodeInternals.values()),
}); });
@@ -33,19 +36,13 @@ const MiniMap = ({
nodeStrokeWidth = 2, nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)', maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => { }: MiniMapProps) => {
const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow); const { viewBBox, nodes } = useStore(selector, shallow);
const elementWidth = (style?.width as number) ?? defaultWidth; const elementWidth = (style?.width as number) ?? defaultWidth;
const elementHeight = (style?.height as number) ?? defaultHeight; const elementHeight = (style?.height as number) ?? defaultHeight;
const nodeColorFunc = getAttrFunction(nodeColor); const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName); const nodeClassNameFunc = getAttrFunction(nodeClassName);
const viewBB: Rect = { const boundingRect = nodes.length > 0 ? getBoundsofRects(getRectOfNodes(nodes), viewBBox) : viewBBox;
x: -transform[0] / transform[2],
y: -transform[1] / transform[2],
width: containerWidth / transform[2],
height: containerHeight / transform[2],
};
const boundingRect = nodes.length > 0 ? getBoundsofRects(getRectOfNodes(nodes), viewBB) : viewBB;
const scaledWidth = boundingRect.width / elementWidth; const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight; const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight); const viewScale = Math.max(scaledWidth, scaledHeight);
@@ -89,7 +86,7 @@ const MiniMap = ({
<path <path
className="react-flow__minimap-mask" className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`} M${viewBBox.x},${viewBBox.y}h${viewBBox.width}v${viewBBox.height}h${-viewBBox.width}z`}
fill={maskColor} fill={maskColor}
fillRule="evenodd" fillRule="evenodd"
/> />

View File

@@ -1,77 +1,70 @@
import React, { useRef, CSSProperties } from 'react'; import React, { CSSProperties, useCallback } from 'react';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge'; import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types'; import { ConnectionLineType, ConnectionLineComponent, HandleType, Position } from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils'; import { internalsSymbol } from '../../utils';
interface ConnectionLineProps { type ConnectionLineProps = {
connectionNodeId: string; connectionNodeId: string;
connectionHandleId: string | null;
connectionHandleType: HandleType; connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
isConnectable: boolean; isConnectable: boolean;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent; CustomConnectionLineComponent?: ConnectionLineComponent;
} };
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform }); const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
export default ({ export default ({
connectionNodeId, connectionNodeId,
connectionHandleId,
connectionHandleType, connectionHandleType,
connectionLineStyle, connectionLineStyle,
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier, connectionLineType = ConnectionLineType.Bezier,
isConnectable, isConnectable,
CustomConnectionLineComponent, CustomConnectionLineComponent,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const nodeId = connectionNodeId; const { fromNode, handleId, toX, toY } = useStore(
const handleId = connectionHandleId; useCallback(
(s) => ({
fromNode: s.nodeInternals.get(connectionNodeId),
handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
}),
[connectionNodeId]
),
shallow
);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const { nodeInternals, transform } = useStore(selector, shallow); if (!fromNode || !isConnectable || !fromHandleBounds?.[connectionHandleType]) {
const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
const fromHandleBounds = fromNode.current?.[internalsSymbol]?.handleBounds;
if (!fromNode.current || !isConnectable || !fromHandleBounds?.[connectionHandleType]) {
return null; return null;
} }
const handleBound = fromHandleBounds[connectionHandleType]; const handleBound = fromHandleBounds[connectionHandleType]!;
const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0]; const fromHandle = handleId ? handleBound.find((d) => d.id === handleId) : handleBound[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX; const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX;
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY; const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY;
const toX = (connectionPositionX - transform[0]) / transform[2];
const toY = (connectionPositionY - transform[1]) / transform[2];
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
let toPosition: Position | undefined; if (!fromPosition) {
switch (fromPosition) { return null;
case Position.Left:
toPosition = Position.Right;
break;
case Position.Right:
toPosition = Position.Left;
break;
case Position.Top:
toPosition = Position.Bottom;
break;
case Position.Bottom:
toPosition = Position.Top;
break;
} }
let toPosition: Position = oppositePosition[fromPosition];
let sourceX: number, let sourceX: number,
sourceY: number, sourceY: number,
sourcePosition: Position | undefined, sourcePosition: Position | undefined,
@@ -106,19 +99,25 @@ export default ({
return ( return (
<g className="react-flow__connection"> <g className="react-flow__connection">
<CustomConnectionLineComponent <CustomConnectionLineComponent
sourceX={sourceX}
sourceY={sourceY}
sourcePosition={sourcePosition}
targetX={targetX}
targetY={targetY}
targetPosition={targetPosition}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
fromNode={fromNode.current} fromNode={fromNode}
fromHandle={fromHandle} fromHandle={fromHandle}
// backward compatibility, mark as deprecated? fromX={fromX}
sourceNode={fromNode.current} fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
// remove in v11
sourcePosition={sourcePosition}
targetPosition={targetPosition}
sourceNode={fromNode}
sourceHandle={fromHandle} sourceHandle={fromHandle}
targetX={targetX}
targetY={targetY}
sourceX={sourceX}
sourceY={sourceY}
/> />
</g> </g>
); );

View File

@@ -1,19 +1,13 @@
import React, { memo, ComponentType, useState, useMemo } from 'react'; import React, { memo, ComponentType, useState, useMemo } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store'; import { useStoreApi } from '../../store';
import { EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types'; import { EdgeProps, WrapEdgeProps, Connection } from '../../types';
import { handleMouseDown } from '../../components/Handle/handler'; import { handleMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph'; import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
id, id,
@@ -56,11 +50,11 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
rfId, rfId,
}: WrapEdgeProps): JSX.Element | null => { }: WrapEdgeProps): JSX.Element | null => {
const [updating, setUpdating] = useState<boolean>(false); const [updating, setUpdating] = useState<boolean>(false);
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const store = useStoreApi(); const store = useStoreApi();
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => { const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const edge = store.getState().edges.find((e) => e.id === id)!; const { edges, addSelectedEdges } = store.getState();
const edge = edges.find((e) => e.id === id)!;
if (elementsSelectable) { if (elementsSelectable) {
store.setState({ nodesSelectionActive: false }); store.setState({ nodesSelectionActive: false });
@@ -86,32 +80,24 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
onEdgeUpdateStart?.(event, edge, handleType); onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdate = onEdgeUpdateEnd const _onEdgeUpdateEnd = onEdgeUpdateEnd
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edge, handleType) ? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edge, handleType)
: undefined; : undefined;
const onConnectEdge = (connection: Connection) => { const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) { handleMouseDown({
onEdgeUpdate(edge, connection);
}
};
handleMouseDown(
event, event,
handleId, handleId,
nodeId, nodeId,
store.setState, onConnect: onConnectEdge,
onConnectEdge,
isTarget, isTarget,
getState: store.getState,
setState: store.setState,
isValidConnection, isValidConnection,
connectionMode, elementEdgeUpdaterType: handleType,
handleType, onEdgeUpdateEnd: _onEdgeUpdateEnd,
_onEdgeUpdate, });
store.getState
);
}; };
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>

View File

@@ -1,17 +1,8 @@
import { MouseEvent as ReactMouseEvent } from 'react'; import { MouseEvent as ReactMouseEvent } from 'react';
import { SetState } from 'zustand'; import { GetState, SetState } from 'zustand';
import { getHostForElement } from '../../utils'; import { getHostForElement } from '../../utils';
import { import { OnConnect, ConnectionMode, Connection, HandleType, ReactFlowState } from '../../types';
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
ConnectionMode,
Connection,
HandleType,
ReactFlowState,
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean; type ValidConnectionFunc = (connection: Connection) => boolean;
@@ -46,6 +37,24 @@ export function checkElementBelowIsValid(
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) { if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
result.isHoveringHandle = true; result.isHoveringHandle = true;
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId,
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId,
};
result.connection = connection;
// in strict mode we don't allow target to target or source to source connections // in strict mode we don't allow target to target or source to source connections
const isValid = const isValid =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
@@ -53,23 +62,6 @@ export function checkElementBelowIsValid(
: true; : true;
if (isValid) { if (isValid) {
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId,
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId,
};
result.connection = connection;
result.isValid = isValidConnection(connection); result.isValid = isValidConnection(connection);
} }
} }
@@ -82,21 +74,29 @@ function resetRecentHandle(hoveredHandle: Element): void {
hoveredHandle?.classList.remove('react-flow__handle-connecting'); hoveredHandle?.classList.remove('react-flow__handle-connecting');
} }
export function handleMouseDown( export function handleMouseDown({
event: ReactMouseEvent, event,
handleId: string | null, handleId,
nodeId: string, nodeId,
setState: SetState<ReactFlowState>, onConnect,
onConnect: OnConnect, isTarget,
isTarget: boolean, getState,
isValidConnection: ValidConnectionFunc, setState,
connectionMode: ConnectionMode, isValidConnection,
elementEdgeUpdaterType?: HandleType, elementEdgeUpdaterType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void, onEdgeUpdateEnd,
onConnectStart?: OnConnectStart, }: {
onConnectStop?: OnConnectStop, event: ReactMouseEvent;
onConnectEnd?: OnConnectEnd handleId: string | null;
): void { nodeId: string;
onConnect: OnConnect;
isTarget: boolean;
getState: GetState<ReactFlowState>;
setState: SetState<ReactFlowState>;
isValidConnection: ValidConnectionFunc;
elementEdgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent) => 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
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
@@ -113,6 +113,7 @@ export function handleMouseDown(
return; return;
} }
const { onConnectStart, connectionMode } = getState();
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source'; const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source';
const containerBounds = reactFlowNode.getBoundingClientRect(); const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element; let recentHoveredHandle: Element;
@@ -151,9 +152,7 @@ export function handleMouseDown(
return resetRecentHandle(recentHoveredHandle); return resetRecentHandle(recentHoveredHandle);
} }
const isOwnHandle = connection.source === connection.target; if (connection.source !== connection.target && elementBelow) {
if (!isOwnHandle && elementBelow) {
recentHoveredHandle = elementBelow; recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting'); elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid); elementBelow.classList.toggle('react-flow__handle-valid', isValid);
@@ -161,6 +160,7 @@ export function handleMouseDown(
} }
function onMouseUp(event: MouseEvent) { function onMouseUp(event: MouseEvent) {
const { onConnectStop, onConnectEnd } = getState();
const { connection, isValid } = checkElementBelowIsValid( const { connection, isValid } = checkElementBelowIsValid(
event, event,
connectionMode, connectionMode,

View File

@@ -14,17 +14,8 @@ const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>; export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
onConnectAction: s.onConnect,
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
onClickConnectStart: s.onClickConnectStart,
onClickConnectStop: s.onClickConnectStop,
onClickConnectEnd: s.onClickConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle, connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick, connectOnClick: s.connectOnClick,
hasDefaultEdges: s.hasDefaultEdges,
}); });
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>( const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
@@ -45,25 +36,13 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
) => { ) => {
const store = useStoreApi(); const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string; const nodeId = useContext(NodeIdContext) as string;
const { const { connectionStartHandle, connectOnClick } = useStore(selector, shallow);
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
hasDefaultEdges,
} = useStore(selector, shallow);
const handleId = id || null; const handleId = id || null;
const isTarget = type === 'target'; const isTarget = type === 'target';
const onConnectExtended = (params: Connection) => { const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions } = store.getState(); const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
const edgeParams = { const edgeParams = {
...defaultEdgeOptions, ...defaultEdgeOptions,
@@ -80,26 +59,22 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const onMouseDownHandler = (event: React.MouseEvent<HTMLDivElement>) => { const onMouseDownHandler = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.button === 0) { if (event.button === 0) {
handleMouseDown( handleMouseDown({
event, event,
handleId, handleId,
nodeId, nodeId,
store.setState, onConnect: onConnectExtended,
onConnectExtended,
isTarget, isTarget,
getState: store.getState,
setState: store.setState,
isValidConnection, isValidConnection,
connectionMode, });
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
} }
onMouseDown?.(event); onMouseDown?.(event);
}; };
const onClick = (event: React.MouseEvent) => { const onClick = (event: React.MouseEvent) => {
const { onClickConnectStart, onClickConnectStop, onClickConnectEnd, connectionMode } = store.getState();
if (!connectionStartHandle) { if (!connectionStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } }); store.setState({ connectionStartHandle: { nodeId, type, handleId } });
@@ -128,28 +103,26 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
store.setState({ connectionStartHandle: null }); store.setState({ connectionStartHandle: null });
}; };
const handleClasses = cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
]);
return ( return (
<div <div
data-handleid={handleId} data-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
className={handleClasses} className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
])}
onMouseDown={onMouseDownHandler} onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined} onClick={connectOnClick ? onClick : undefined}
ref={ref} ref={ref}

View File

@@ -4,20 +4,10 @@ import { GetState, SetState } from 'zustand';
import { HandleElement, Node, Position, ReactFlowState } from '../../types'; import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils'; import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => { export const getHandleBounds = (
const bounds = nodeElement.getBoundingClientRect();
return {
source: getHandleBoundsByHandleType('.source', nodeElement, bounds, scale),
target: getHandleBoundsByHandleType('.target', nodeElement, bounds, scale),
};
};
export const getHandleBoundsByHandleType = (
selector: string, selector: string,
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
parentBounds: DOMRect, zoom: number
k: number
): HandleElement[] | null => { ): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(selector);
@@ -26,19 +16,17 @@ export const getHandleBoundsByHandleType = (
} }
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
const nodeBounds = nodeElement.getBoundingClientRect();
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return { return {
id: handleId, id: handle.getAttribute('data-handleid'),
position: handlePosition, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (bounds.left - parentBounds.left) / k, x: (handleBounds.left - nodeBounds.left) / zoom,
y: (bounds.top - parentBounds.top) / k, y: (handleBounds.top - nodeBounds.top) / zoom,
...dimensions, ...getDimensions(handle),
}; };
}); });
}; };

View File

@@ -1,14 +1,12 @@
import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react'; import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { useStore, useStoreApi } from '../../store'; import { useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext'; import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types'; import { NodeProps, WrapNodeProps } from '../../types';
import useDrag from '../../hooks/useDrag'; import useDrag from '../../hooks/useDrag';
import { getMouseHandler, handleNodeClick } from './utils'; import { getMouseHandler, handleNodeClick } from './utils';
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default (NodeComponent: ComponentType<NodeProps>) => { export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({ const NodeWrapper = ({
id, id,
@@ -23,9 +21,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onMouseLeave, onMouseLeave,
onContextMenu, onContextMenu,
onDoubleClick, onDoubleClick,
onDragStart,
onDrag,
onDragStop,
style, style,
className, className,
isDraggable, isDraggable,
@@ -44,7 +39,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
initialized, initialized,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const store = useStoreApi(); const store = useStoreApi();
const updateNodeDimensions = useStore(selector);
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition); const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition); const prevTargetPosition = useRef(targetPosition);
@@ -96,14 +90,11 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
if (targetPosChanged) { if (targetPosChanged) {
prevTargetPosition.current = targetPosition; prevTargetPosition.current = targetPosition;
} }
updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]); store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
} }
}, [id, type, sourcePosition, targetPosition]); }, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({ const dragging = useDrag({
onStart: onDragStart,
onDrag: onDrag,
onStop: onDragStop,
nodeRef, nodeRef,
disabled: hidden || !isDraggable, disabled: hidden || !isDraggable,
noDragClassName, noDragClassName,

View File

@@ -3,7 +3,7 @@
* made a selection with on or several nodes * made a selection with on or several nodes
*/ */
import React, { memo, useCallback, useRef, MouseEvent } from 'react'; import React, { memo, useRef, MouseEvent } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
@@ -13,16 +13,14 @@ import { getRectOfNodes } from '../../utils/graph';
import useDrag from '../../hooks/useDrag'; import useDrag from '../../hooks/useDrag';
export interface NodesSelectionProps { export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string; noPanClassName?: string;
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
transform: s.transform, transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`,
userSelectionActive: s.userSelectionActive, userSelectionActive: s.userSelectionActive,
...getRectOfNodes(Array.from(s.nodeInternals.values()).filter((n) => n.selected)),
}); });
const bboxSelector = (s: ReactFlowState) => { const bboxSelector = (s: ReactFlowState) => {
@@ -30,31 +28,14 @@ const bboxSelector = (s: ReactFlowState) => {
return getRectOfNodes(selectedNodes); return getRectOfNodes(selectedNodes);
}; };
function useGetMemoizedHandler(handler?: (event: MouseEvent, nodes: Node[]) => void) { function NodesSelection({ onSelectionContextMenu, noPanClassName }: NodesSelectionProps) {
return useCallback((event: MouseEvent, _: Node, nodes: Node[]) => handler?.(event, nodes), [handler]);
}
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) {
const store = useStoreApi(); const store = useStoreApi();
const { transform, userSelectionActive } = useStore(selector, shallow); const { transformString, userSelectionActive } = useStore(selector, shallow);
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow); const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const nodeRef = useRef(null); const nodeRef = useRef(null);
// it's important that these handlers are memoized to avoid multiple creation of d3 drag handler
const onStart = useGetMemoizedHandler(onSelectionDragStart);
const onDrag = useGetMemoizedHandler(onSelectionDrag);
const onStop = useGetMemoizedHandler(onSelectionDragStop);
useDrag({ useDrag({
onStart,
onDrag,
onStop,
nodeRef, nodeRef,
}); });
@@ -73,7 +54,7 @@ function NodesSelection({
<div <div
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
style={{ style={{
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, transform: transformString,
}} }}
> >
<div <div

View File

@@ -3,56 +3,46 @@ import { SetState } from 'zustand';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store'; import { useStore, useStoreApi } from '../../store';
import { import { Node, Edge, ReactFlowState, CoordinateExtent, ReactFlowProps } from '../../types';
Node,
Edge,
ReactFlowState,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
CoordinateExtent,
OnNodesChange,
OnEdgesChange,
ConnectionMode,
SnapGrid,
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete,
} from '../../types';
interface StoreUpdaterProps { type StoreUpdaterProps = Pick<
nodes?: Node[]; ReactFlowProps,
edges?: Edge[]; | 'nodes'
defaultNodes?: Node[]; | 'edges'
defaultEdges?: Edge[]; | 'defaultNodes'
onConnect?: OnConnect; | 'defaultEdges'
onConnectStart?: OnConnectStart; | 'onConnect'
onConnectStop?: OnConnectStop; | 'onConnectStart'
onConnectEnd?: OnConnectEnd; | 'onConnectStop'
onClickConnectStart?: OnConnectStart; | 'onConnectEnd'
onClickConnectStop?: OnConnectStop; | 'onClickConnectStart'
onClickConnectEnd?: OnConnectEnd; | 'onClickConnectStop'
nodesDraggable?: boolean; | 'onClickConnectEnd'
nodesConnectable?: boolean; | 'nodesDraggable'
minZoom?: number; | 'nodesConnectable'
maxZoom?: number; | 'minZoom'
nodeExtent?: CoordinateExtent; | 'maxZoom'
onNodesChange?: OnNodesChange; | 'nodeExtent'
onEdgesChange?: OnEdgesChange; | 'onNodesChange'
elementsSelectable?: boolean; | 'onEdgesChange'
connectionMode?: ConnectionMode; | 'elementsSelectable'
snapToGrid?: boolean; | 'connectionMode'
snapGrid?: SnapGrid; | 'snapToGrid'
translateExtent?: CoordinateExtent; | 'snapGrid'
connectOnClick: boolean; | 'translateExtent'
defaultEdgeOptions?: DefaultEdgeOptions; | 'connectOnClick'
fitView?: boolean; | 'defaultEdgeOptions'
fitViewOptions?: FitViewOptions; | 'fitView'
onNodesDelete?: OnNodesDelete; | 'fitViewOptions'
onEdgesDelete?: OnEdgesDelete; | 'onNodesDelete'
} | 'onEdgesDelete'
| 'onNodeDragStart'
| 'onNodeDrag'
| 'onNodeDragStop'
| 'onSelectionDragStart'
| 'onSelectionDrag'
| 'onSelectionDragStop'
>;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes, setNodes: s.setNodes,
@@ -112,6 +102,12 @@ const StoreUpdater = ({
fitViewOptions, fitViewOptions,
onNodesDelete, onNodesDelete,
onEdgesDelete, onEdgesDelete,
onNodeDrag,
onNodeDragStart,
onNodeDragStop,
onSelectionDrag,
onSelectionDragStart,
onSelectionDragStop,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
@@ -154,6 +150,12 @@ const StoreUpdater = ({
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState); useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState); useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState); useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useDirectStoreUpdater('onNodeDrag', onNodeDrag, store.setState);
useDirectStoreUpdater('onNodeDragStart', onNodeDragStart, store.setState);
useDirectStoreUpdater('onNodeDragStop', onNodeDragStop, store.setState);
useDirectStoreUpdater('onSelectionDrag', onSelectionDrag, store.setState);
useDirectStoreUpdater('onSelectionDragStart', onSelectionDragStart, store.setState);
useDirectStoreUpdater('onSelectionDragStop', onSelectionDragStop, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges); useStoreUpdater<Edge[]>(edges, setEdges);

View File

@@ -45,9 +45,7 @@ interface EdgeRendererProps {
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId, connectionNodeId: s.connectionNodeId,
connectionHandleId: s.connectionHandleId,
connectionHandleType: s.connectionHandleType, connectionHandleType: s.connectionHandleType,
connectionPosition: s.connectionPosition,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
width: s.width, width: s.width,
@@ -59,9 +57,7 @@ const selector = (s: ReactFlowState) => ({
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = (props: EdgeRendererProps) => {
const { const {
connectionNodeId, connectionNodeId,
connectionHandleId,
connectionHandleType, connectionHandleType,
connectionPosition,
nodesConnectable, nodesConnectable,
elementsSelectable, elementsSelectable,
width, width,
@@ -201,10 +197,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
> >
<ConnectionLine <ConnectionLine
connectionNodeId={connectionNodeId!} connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!} connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
isConnectable={nodesConnectable} isConnectable={nodesConnectable}

View File

@@ -1,5 +1,4 @@
import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react'; import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store'; import { useStore, useStoreApi } from '../../store';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
@@ -27,10 +26,7 @@ export type FlowRendererProps = Omit<
children: ReactNode; children: ReactNode;
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => s.nodesSelectionActive;
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
const FlowRenderer = ({ const FlowRenderer = ({
children, children,
@@ -55,27 +51,24 @@ const FlowRenderer = ({
defaultPosition, defaultPosition,
defaultZoom, defaultZoom,
preventScrolling, preventScrolling,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu, onSelectionContextMenu,
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
}: FlowRendererProps) => { }: FlowRendererProps) => {
const store = useStoreApi(); const store = useStoreApi();
const { resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); const nodesSelectionActive = useStore(selector);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode }); useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = (event: MouseEvent) => { const onClick = (event: MouseEvent) => {
onPaneClick?.(event); onPaneClick?.(event);
resetSelectedElements(); store.getState().resetSelectedElements();
store.setState({ nodesSelectionActive: false }); store.setState({ nodesSelectionActive: false });
}; };
const onContextMenu = (event: MouseEvent) => onPaneContextMenu?.(event);
const onWheel = (event: WheelEvent) => onPaneScroll?.(event); const onContextMenu = onPaneContextMenu ? (event: MouseEvent) => onPaneContextMenu(event) : undefined;
const onWheel = onPaneScroll ? (event: WheelEvent) => onPaneScroll(event) : undefined;
return ( return (
<ZoomPane <ZoomPane
@@ -101,13 +94,7 @@ const FlowRenderer = ({
{children} {children}
<UserSelection selectionKeyPressed={selectionKeyPressed} /> <UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && ( {nodesSelectionActive && (
<NodesSelection <NodesSelection onSelectionContextMenu={onSelectionContextMenu} noPanClassName={noPanClassName} />
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
noPanClassName={noPanClassName}
/>
)} )}
<div <div
className="react-flow__pane react-flow__container" className="react-flow__pane react-flow__container"

View File

@@ -40,12 +40,6 @@ const GraphView = ({
onNodeMouseMove, onNodeMouseMove,
onNodeMouseLeave, onNodeMouseLeave,
onNodeContextMenu, onNodeContextMenu,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu, onSelectionContextMenu,
connectionLineType, connectionLineType,
connectionLineStyle, connectionLineStyle,
@@ -110,9 +104,6 @@ const GraphView = ({
panOnDrag={panOnDrag} panOnDrag={panOnDrag}
defaultPosition={defaultPosition} defaultPosition={defaultPosition}
defaultZoom={defaultZoom} defaultZoom={defaultZoom}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu} onSelectionContextMenu={onSelectionContextMenu}
preventScrolling={preventScrolling} preventScrolling={preventScrolling}
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
@@ -150,9 +141,6 @@ const GraphView = ({
onNodeMouseMove={onNodeMouseMove} onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave} onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu} onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag} selectNodesOnDrag={selectNodesOnDrag}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}

View File

@@ -3,14 +3,7 @@ import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { import { NodeMouseHandler, NodeTypesWrapped, Position, ReactFlowState, WrapNodeProps } from '../../types';
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
import { internalsSymbol } from '../../utils'; import { internalsSymbol } from '../../utils';
interface NodeRendererProps { interface NodeRendererProps {
@@ -22,9 +15,6 @@ interface NodeRendererProps {
onNodeMouseMove?: NodeMouseHandler; onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler; onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler; onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean; onlyRenderVisibleElements: boolean;
noPanClassName: string; noPanClassName: string;
noDragClassName: string; noDragClassName: string;
@@ -109,9 +99,6 @@ const NodeRenderer = (props: NodeRendererProps) => {
onMouseLeave={props.onNodeMouseLeave} onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu} onContextMenu={props.onNodeContextMenu}
onDoubleClick={props.onNodeDoubleClick} onDoubleClick={props.onNodeDoubleClick}
onDragStart={props.onNodeDragStart}
onDrag={props.onNodeDrag}
onDragStop={props.onNodeDragStop}
selected={!!node.selected} selected={!!node.selected}
isDraggable={isDraggable} isDraggable={isDraggable}
isSelectable={isSelectable} isSelectable={isSelectable}

View File

@@ -165,9 +165,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onNodeMouseLeave={onNodeMouseLeave} onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu} onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick} onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesWrapped} nodeTypes={nodeTypesWrapped}
edgeTypes={edgeTypesWrapped} edgeTypes={edgeTypesWrapped}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
@@ -193,9 +190,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll} onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu} onPaneContextMenu={onPaneContextMenu}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu} onSelectionContextMenu={onSelectionContextMenu}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
@@ -243,6 +237,12 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
fitViewOptions={fitViewOptions} fitViewOptions={fitViewOptions}
onNodesDelete={onNodesDelete} onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete} onEdgesDelete={onEdgesDelete}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
onSelectionDrag={onSelectionDrag}
onSelectionDragStart={onSelectionDragStart}
onSelectionDragStop={onSelectionDragStop}
/> />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />} {onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children} {children}

View File

@@ -3,7 +3,7 @@ import React, { ReactNode } from 'react';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { ReactFlowState } from '../../types'; import { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.transform; const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
type ViewportProps = { type ViewportProps = {
children: ReactNode; children: ReactNode;
@@ -13,10 +13,7 @@ function Viewport({ children }: ViewportProps) {
const transform = useStore(selector); const transform = useStore(selector);
return ( return (
<div <div className="react-flow__viewport react-flow__container" style={{ transform: transform }}>
className="react-flow__viewport react-flow__container"
style={{ transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` }}
>
{children} {children}
</div> </div>
); );

View File

@@ -54,6 +54,7 @@ const ZoomPane = ({
noPanClassName, noPanClassName,
}: ZoomPaneProps) => { }: ZoomPaneProps) => {
const store = useStoreApi(); const store = useStoreApi();
const isZoomingOrPanning = useRef(false);
const zoomPane = useRef<HTMLDivElement>(null); const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 }); const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 });
const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow); const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow);
@@ -146,12 +147,11 @@ const ZoomPane = ({
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (selectionKeyPressed) { if (selectionKeyPressed && !isZoomingOrPanning.current) {
d3Zoom.on('zoom', null); d3Zoom.on('zoom', null);
} else { } else if (!selectionKeyPressed) {
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => { d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] }); store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] });
if (onMove) { if (onMove) {
const flowTransform = eventToFlowTransform(event.transform); const flowTransform = eventToFlowTransform(event.transform);
onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
@@ -163,33 +163,31 @@ const ZoomPane = ({
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (onMoveStart) { d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => { isZoomingOrPanning.current = true;
if (onMoveStart) {
const flowTransform = eventToFlowTransform(event.transform); const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform; prevTransform.current = flowTransform;
onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}); }
} else { });
d3Zoom.on('start', null);
}
} }
}, [d3Zoom, onMoveStart]); }, [d3Zoom, onMoveStart]);
useEffect(() => { useEffect(() => {
if (d3Zoom) { if (d3Zoom) {
if (onMoveEnd) { d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => { isZoomingOrPanning.current = false;
if (viewChanged(prevTransform.current, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); if (onMoveEnd && viewChanged(prevTransform.current, event.transform)) {
} const flowTransform = eventToFlowTransform(event.transform);
}); prevTransform.current = flowTransform;
} else {
d3Zoom.on('end', null); onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
} }
});
} }
}, [d3Zoom, onMoveEnd]); }, [d3Zoom, onMoveEnd]);

View File

@@ -4,7 +4,7 @@ import { select } from 'd3-selection';
import { useStoreApi } from '../../store'; import { useStoreApi } from '../../store';
import { pointToRendererPoint } from '../../utils/graph'; import { pointToRendererPoint } from '../../utils/graph';
import { NodeDragItem, NodeDragHandler } from '../../types'; import { NodeDragItem, Node, SelectionDragHandler } from '../../types';
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from './utils'; import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils'; import { handleNodeClick } from '../../components/Nodes/utils';
@@ -13,9 +13,6 @@ export type UseDragData = { dx: number; dy: number };
type UseDragParams = { type UseDragParams = {
nodeRef: RefObject<Element>; nodeRef: RefObject<Element>;
onStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onStop?: NodeDragHandler;
disabled?: boolean; disabled?: boolean;
noDragClassName?: string; noDragClassName?: string;
handleSelector?: string; handleSelector?: string;
@@ -24,10 +21,11 @@ type UseDragParams = {
selectNodesOnDrag?: boolean; selectNodesOnDrag?: boolean;
}; };
function wrapSelectionDragFunc(selectionFunc?: SelectionDragHandler) {
return (event: MouseEvent, _: Node, nodes: Node[]) => selectionFunc?.(event, nodes);
}
function useDrag({ function useDrag({
onStart,
onDrag,
onStop,
nodeRef, nodeRef,
disabled = false, disabled = false,
noDragClassName, noDragClassName,
@@ -60,7 +58,15 @@ function useDrag({
} else { } else {
const dragHandler = drag() const dragHandler = drag()
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const { nodeInternals, multiSelectionActive, unselectNodesAndEdges } = store.getState(); const {
nodeInternals,
multiSelectionActive,
unselectNodesAndEdges,
onNodeDragStart,
onSelectionDragStart,
} = store.getState();
const onStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
if (!selectNodesOnDrag && !multiSelectionActive && nodeId) { if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
if (!nodeInternals.get(nodeId)?.selected) { if (!nodeInternals.get(nodeId)?.selected) {
@@ -77,6 +83,7 @@ function useDrag({
} }
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
lastPos.current = pointerPos;
dragItems.current = getDragItems(nodeInternals, pointerPos, nodeId); dragItems.current = getDragItems(nodeInternals, pointerPos, nodeId);
if (onStart && dragItems.current) { if (onStart && dragItems.current) {
@@ -89,7 +96,7 @@ function useDrag({
} }
}) })
.on('drag', (event: UseDragEvent) => { .on('drag', (event: UseDragEvent) => {
const { updateNodePositions, nodeInternals, nodeExtent } = store.getState(); const { updateNodePositions, nodeInternals, nodeExtent, onNodeDrag, onSelectionDrag } = store.getState();
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
// skip events without movement // skip events without movement
@@ -99,6 +106,8 @@ function useDrag({
updatePosition(n, pointerPos, nodeInternals, nodeExtent) updatePosition(n, pointerPos, nodeInternals, nodeExtent)
); );
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
updateNodePositions(dragItems.current, true, true); updateNodePositions(dragItems.current, true, true);
setDragging(true); setDragging(true);
@@ -115,6 +124,9 @@ function useDrag({
event.on('end', (event) => { event.on('end', (event) => {
setDragging(false); setDragging(false);
if (dragItems.current) { if (dragItems.current) {
const { updateNodePositions, nodeInternals, onNodeDragStop, onSelectionDragStop } = store.getState();
const onStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
updateNodePositions(dragItems.current, false, false); updateNodePositions(dragItems.current, false, false);
if (onStop) { if (onStop) {
@@ -146,9 +158,6 @@ function useDrag({
} }
} }
}, [ }, [
onStart,
onDrag,
onStop,
nodeRef, nodeRef,
disabled, disabled,
noDragClassName, noDragClassName,

View File

@@ -39,7 +39,8 @@ export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition,
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals))) .filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals)))
.map((n) => ({ .map((n) => ({
id: n.id, id: n.id,
position: n.positionAbsolute || { x: 0, y: 0 }, position: n.position || { x: 0, y: 0 },
positionAbsolute: n.positionAbsolute || { x: 0, y: 0 },
distance: { distance: {
x: mousePos.x - (n.positionAbsolute?.x ?? 0), x: mousePos.x - (n.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.positionAbsolute?.y ?? 0), y: mousePos.y - (n.positionAbsolute?.y ?? 0),
@@ -94,14 +95,28 @@ export function updatePosition(
]; ];
} }
dragItem.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition; let parentPosition = { x: 0, y: 0 };
if (dragItem.parentNode) {
const parentNode = nodeInternals.get(dragItem.parentNode);
parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 };
}
dragItem.positionAbsolute = currentExtent
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
: nextPosition;
dragItem.position = {
x: dragItem.positionAbsolute.x - parentPosition.x,
y: dragItem.positionAbsolute.y - parentPosition.y,
};
return dragItem; return dragItem;
} }
// returns two params: // returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection) // 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (handy when multi selection is active) // 2. array of selected nodes (for multi selections)
export function getEventHandlerParams({ export function getEventHandlerParams({
nodeId, nodeId,
dragItems, dragItems,
@@ -116,6 +131,8 @@ export function getEventHandlerParams({
return { return {
...node, ...node,
position: n.position,
positionAbsolute: n.positionAbsolute,
}; };
}); });

View File

@@ -38,12 +38,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2), zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2),
zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2), zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2),
zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel), zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel),
getZoom: () => { getZoom: () => store.getState().transform[2],
const [, , zoom] = store.getState().transform;
return zoom;
},
setViewport: (transform, options) => { setViewport: (transform, options) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); const [x, y, zoom] = store.getState().transform;
const nextTransform = zoomIdentity
.translate(transform.x ?? x, transform.y ?? y)
.scale(transform.zoom ?? zoom);
d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform); d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform);
}, },
getViewport: () => { getViewport: () => {

View File

@@ -8,11 +8,10 @@ import { ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) { function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore( const nodes = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) =>
return onlyRenderVisible onlyRenderVisible
? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) ? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: Array.from(s.nodeInternals.values()); : Array.from(s.nodeInternals.values()),
},
[onlyRenderVisible] [onlyRenderVisible]
) )
); );

View File

@@ -43,7 +43,10 @@ const createStore = () =>
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
}, },
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, transform, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get(); const { onNodesChange, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get();
const style = window.getComputedStyle(document.querySelector('.react-flow__viewport')!);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => { const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = nodeInternals.get(update.id); const node = nodeInternals.get(update.id);
@@ -57,12 +60,14 @@ const createStore = () =>
); );
if (doUpdate) { if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, transform[2]);
nodeInternals.set(node.id, { nodeInternals.set(node.id, {
...node, ...node,
[internalsSymbol]: { [internalsSymbol]: {
...node[internalsSymbol], ...node[internalsSymbol],
handleBounds, handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom),
target: getHandleBounds('.target', update.nodeElement, zoom),
},
}, },
...dimensions, ...dimensions,
}); });
@@ -99,16 +104,8 @@ const createStore = () =>
}; };
if (positionChanged) { if (positionChanged) {
change.positionAbsolute = node.position; change.positionAbsolute = node.positionAbsolute;
change.position = node.position; change.position = node.position;
if (node.parentNode) {
const parentNode = nodeInternals.get(node.parentNode);
change.position = {
x: change.position.x - (parentNode?.positionAbsolute?.x ?? 0),
y: change.position.y - (parentNode?.positionAbsolute?.y ?? 0),
};
}
} }
return change; return change;

View File

@@ -38,6 +38,7 @@ import {
OnMoveEnd, OnMoveEnd,
NodeDragHandler, NodeDragHandler,
NodeMouseHandler, NodeMouseHandler,
SelectionDragHandler,
} from '.'; } from '.';
import { HandleType } from './handles'; import { HandleType } from './handles';
@@ -73,9 +74,9 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
onMoveStart?: OnMoveStart; onMoveStart?: OnMoveStart;
onMoveEnd?: OnMoveEnd; onMoveEnd?: OnMoveEnd;
onSelectionChange?: OnSelectionChangeFunc; onSelectionChange?: OnSelectionChangeFunc;
onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDragStart?: SelectionDragHandler;
onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDrag?: SelectionDragHandler;
onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDragStop?: SelectionDragHandler;
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void;
onPaneScroll?: (event?: WheelEvent) => void; onPaneScroll?: (event?: WheelEvent) => void;
onPaneClick?: (event: ReactMouseEvent) => void; onPaneClick?: (event: ReactMouseEvent) => void;

View File

@@ -148,17 +148,23 @@ export enum ConnectionLineType {
} }
export type ConnectionLineComponentProps = { export type ConnectionLineComponentProps = {
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;
fromNode?: Node;
fromHandle?: HandleElement;
fromX: number;
fromY: number;
toX: number;
toY: number;
fromPosition: Position;
toPosition: Position;
// remove in v11
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
sourcePosition?: Position; sourcePosition?: Position;
targetX: number; targetX: number;
targetY: number; targetY: number;
targetPosition?: Position; targetPosition?: Position;
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;
fromNode?: Node;
fromHandle?: HandleElement;
// backward compatibility, mark as deprecated?
sourceNode?: Node; sourceNode?: Node;
sourceHandle?: HandleElement; sourceHandle?: HandleElement;
}; };

View File

@@ -3,7 +3,16 @@ import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes'; import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeProps, WrapNodeProps, NodeDragItem } from './nodes'; import {
Node,
NodeInternals,
NodeDimensionUpdate,
NodeProps,
WrapNodeProps,
NodeDragItem,
NodeDragHandler,
SelectionDragHandler,
} from './nodes';
import { Edge, EdgeProps, WrapEdgeProps } from './edges'; import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles'; import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.'; import { DefaultEdgeOptions } from '.';
@@ -165,6 +174,14 @@ export type ReactFlowStore = {
connectionStartHandle: StartHandle | null; connectionStartHandle: StartHandle | null;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onSelectionDragStart?: SelectionDragHandler;
onSelectionDrag?: SelectionDragHandler;
onSelectionDragStop?: SelectionDragHandler;
onConnect?: OnConnect; onConnect?: OnConnect;
onConnectStart?: OnConnectStart; onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop; onConnectStop?: OnConnectStop;

View File

@@ -55,6 +55,7 @@ export interface NodeProps<T = any> {
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void; export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void;
export interface WrapNodeProps<T = any> { export interface WrapNodeProps<T = any> {
id: string; id: string;
@@ -74,9 +75,6 @@ export interface WrapNodeProps<T = any> {
onMouseMove?: NodeMouseHandler; onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler; onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler; onContextMenu?: NodeMouseHandler;
onDragStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onDragStop?: NodeDragHandler;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourcePosition: Position; sourcePosition: Position;
@@ -110,8 +108,8 @@ export type NodeBounds = XYPosition & {
export type NodeDragItem = { export type NodeDragItem = {
id: string; id: string;
// relative node position
position: XYPosition; position: XYPosition;
positionAbsolute: XYPosition;
// distance from the mouse cursor to the node when start dragging // distance from the mouse cursor to the node when start dragging
distance: XYPosition; distance: XYPosition;
width?: number | null; width?: number | null;

View File

@@ -10,17 +10,14 @@ function handleParentExpand(res: any[], updateItem: any) {
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) { if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
parent.style = { ...parent.style } || {}; parent.style = { ...parent.style } || {};
parent.style.width = parent.style.width ?? parent.width;
parent.style.height = parent.style.height ?? parent.height;
if (extendWidth > 0) { if (extendWidth > 0) {
if (!parent.style.width) {
parent.style.width = parent.width;
}
parent.style.width += extendWidth; parent.style.width += extendWidth;
} }
if (extendHeight > 0) { if (extendHeight > 0) {
if (!parent.style.height) {
parent.style.height = parent.height;
}
parent.style.height += extendHeight; parent.style.height += extendHeight;
} }

6680
yarn.lock

File diff suppressed because it is too large Load Diff