Merge pull request #2764 from wbkd/next-release

Next release: 11.5.0
This commit is contained in:
Moritz Klack
2023-01-26 11:54:30 +01:00
committed by GitHub
65 changed files with 1081 additions and 541 deletions
+15 -13
View File
@@ -51,16 +51,16 @@ describe('Basic Flow Rendering', () => {
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 1000, 50, { button: 0, force: true })
.trigger('mousemove', 1, 400, { button: 0 })
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 })
.wait(50)
.trigger('mouseup', 1, 200, { force: true });
.trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(100);
cy.wait(200);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.not.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
@@ -74,11 +74,13 @@ describe('Basic Flow Rendering', () => {
.trigger('mousedown', 'topRight', { button: 0, force: true })
.trigger('mousemove', 'bottomLeft', { button: 0 })
.wait(50)
.trigger('mouseup', 'bottomLeft', { force: true })
.wait(50)
.trigger('mouseup', 'bottomLeft', { button: 0, force: true })
.wait(400)
.get('.react-flow__node')
.should('have.class', 'selected')
.get('.react-flow__nodesselection-rect');
.should('have.class', 'selected');
cy.wait(200);
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true });
});
@@ -113,14 +115,14 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { button: 0 });
.trigger('mousedown', { force: true, button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove', { force: true })
.wait(50)
.trigger('mouseup', { force: true });
.trigger('mousemove', { force: true, button: 0 })
.wait(200)
.trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').should('have.length', 3);
});
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000 --open",
"dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000",
"build": "vite build",
"test:dev": "cypress open",
@@ -48,8 +48,6 @@ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3' },
];
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const defaultEdgeOptions = {};
const BasicFlow = () => {
@@ -96,7 +94,6 @@ const BasicFlow = () => {
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
// nodeOrigin={nodeOrigin}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -1,9 +1,8 @@
import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'reactflow';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => {
function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
return (
<g>
<>
<path
fill="none"
stroke="#222"
@@ -12,8 +11,8 @@ const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, t
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g>
</>
);
};
}
export default ConnectionLine;
@@ -1,4 +1,4 @@
import React, { FC, useMemo, CSSProperties } from 'react';
import { FC, useMemo, CSSProperties } from 'react';
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'reactflow';
import { getEdgeParams } from './utils';
@@ -17,7 +17,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const d = getBezierPath({
const [path] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
@@ -28,7 +28,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
return (
<g className="react-flow__connection">
<path id={id} className="react-flow__edge-path" d={d} style={style as CSSProperties} />
<path id={id} className="react-flow__edge-path" d={path} style={style as CSSProperties} />
</g>
);
};
@@ -1,7 +1,7 @@
import { memo, FC, CSSProperties } from 'react';
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer';
import { NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
import ResizeIcon from './ResizeIcon';
@@ -1,14 +1,14 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer';
import { NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl color="red" position="top" />
<NodeResizeControl color="red" position="bottom" />
<NodeResizeControl color="red" position={Position.Top} />
<NodeResizeControl color="red" position={Position.Bottom} />
<Handle type="target" position={Position.Left} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Right} />
@@ -0,0 +1,33 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizeControl, OnResize, ShouldResize } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const shouldResize: ShouldResize = (event, params) => {
console.log('before resize', params);
if (params.width > 100) {
return false;
}
return true;
};
const onResize: OnResize = (event, params) => {
console.log('resize', params.direction);
};
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl color="red" position={Position.Left} />
<NodeResizeControl color="red" position={Position.Right} shouldResize={shouldResize} onResize={onResize} />
<Handle type="target" position={Position.Top} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(CustomNode);
@@ -1,21 +1,27 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, ResizeDragEvent, ResizeEventParams } from '@reactflow/node-resizer';
import { NodeResizer, ShouldResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const onResizeStart = (_: ResizeDragEvent, params: ResizeEventParams) => {
const onResizeStart: OnResizeStart = (_, params) => {
console.log('resize start', params);
};
const onResize = (_: ResizeDragEvent, params: ResizeEventParams) => {
const onResize: OnResize = (_, params) => {
console.log('resize', params);
};
const onResizeEnd = (_: ResizeDragEvent, params: ResizeEventParams) => {
const onResizeEnd: OnResizeEnd = (_, params) => {
console.log('resize end', params);
};
const shouldResize: ShouldResize = (_, params) => {
console.log('should resize', params);
return true;
};
const CustomNode: FC<NodeProps> = ({ data, selected }) => {
return (
<>
@@ -23,6 +29,7 @@ const CustomNode: FC<NodeProps> = ({ data, selected }) => {
minWidth={100}
minHeight={100}
isVisible={selected}
shouldResize={shouldResize}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeEnd={onResizeEnd}
@@ -4,11 +4,13 @@ import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useE
import NodeResizerNode from './NodeResizerNode';
import CustomResizer from './CustomResizer';
import CustomResizer2 from './CustomResizer2';
import CustomResizer3 from './CustomResizer3';
const nodeTypes = {
resizer: NodeResizerNode,
customResizer: CustomResizer,
customResizer2: CustomResizer2,
customResizer3: CustomResizer3,
};
const initialEdges = [
@@ -60,6 +62,13 @@ const initialNodes = [
position: { x: 250, y: 250 },
style: { border: '1px solid #222', fontSize: 10 },
},
{
id: '6',
type: 'customResizer3',
data: { label: 'resize controls' },
position: { x: 400, y: 200 },
style: { border: '1px solid #222', fontSize: 10 },
},
];
const CustomNodeFlow = () => {
@@ -87,7 +96,7 @@ const CustomNodeFlow = () => {
>
<Controls />
<Panel position="bottom-right">
<button onClick={() => setSnapToGrid(!snapToGrid)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button>
<button onClick={() => setSnapToGrid((s) => !s)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button>
</Panel>
</ReactFlow>
);
@@ -12,6 +12,7 @@ import ReactFlow, {
MiniMap,
Background,
Panel,
NodeOrigin,
} from 'reactflow';
import DebugNode from './DebugNode';
@@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react';
import { MouseEvent, useCallback } from 'react';
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
@@ -35,7 +35,7 @@ const nodesA: Node[] = [
];
const edgesA: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: null },
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
@@ -83,6 +83,8 @@ const edgesB: Edge[] = [
{ id: 'e4b', source: 'inputb', target: '4b' },
];
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node.position);
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
@@ -98,7 +100,7 @@ const BasicFlow = () => {
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
disableKeyboardA11y
onNodeDrag={onNodeDrag}
>
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button
@@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react';
import { MouseEvent, useCallback } from 'react';
import ReactFlow, {
useReactFlow,
NodeTypes,
@@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els));
+7
View File
@@ -1,5 +1,12 @@
# @reactflow/background
## 11.1.3
### Patch Changes
- Updated dependencies [[`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694), [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52), [`4c516882`](https://github.com/wbkd/react-flow/commit/4c516882d2bbf426c1832a53ad40763cc1abef92)]:
- @reactflow/core@11.5.0
## 11.1.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@reactflow/background",
"version": "11.1.2",
"version": "11.1.3",
"description": "Background component with different variants for React Flow",
"keywords": [
"react",
+7
View File
@@ -1,5 +1,12 @@
# @reactflow/controls
## 11.1.3
### Patch Changes
- Updated dependencies [[`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694), [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52), [`4c516882`](https://github.com/wbkd/react-flow/commit/4c516882d2bbf426c1832a53ad40763cc1abef92)]:
- @reactflow/core@11.5.0
## 11.1.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@reactflow/controls",
"version": "11.1.2",
"version": "11.1.3",
"description": "Component to control the viewport of a React Flow instance",
"keywords": [
"react",
+25
View File
@@ -1,5 +1,30 @@
# @reactflow/core
## 11.5.0
Lot's of improvements are coming with this release!
- **Connecting radius**: No need to drop a connection line on top of handle anymore. You only need to be close to the handle. That radius can be configured with the `connectionRadius` prop.
- **Auto pan**: When you drag a node, a selection or the connection line to the border of the pane, it will pan into that direction. That makes it easier to connect far away nodes for example. If you don't like it you can set `autoPnaOnNodeDrag` and `autoPanOnConnect` to false.
- **Touch devices**: It's finally possibleto connect nodes with the connection line on touch devices. In combination with the new auto pan and connection radius the overall UX is way better.
- **Errors**: We added an `onError` prop to get notified when an error like "couldn't find source handle" happens. This is useful if you want to log errors for example.
- **Node type**: We added a second param to the generic `Node` type. You can not only pass `NodeData` but also the type as a second param:
```ts
type MyCustomNode = Node<MyCustomNodeData, 'custom-node-type'>
```
This makes it easier to work with different custom nodes and data types.
### Minor Changes
- [#2754](https://github.com/wbkd/react-flow/pull/2754) [`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694) - Add auto pan for connecting and node dragging and `connectionRadius`
- [#2773](https://github.com/wbkd/react-flow/pull/2773) - Add `onError` prop to get notified when an error happens
### Patch Changes
- [#2763](https://github.com/wbkd/react-flow/pull/2763) [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52) - Connecting nodes: Enable connections on touch devices
- [#2620](https://github.com/wbkd/react-flow/pull/2620) - Thanks [RichSchulz](https://github.com/RichSchulz)! - Types: improve typing for node type
## 11.4.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@reactflow/core",
"version": "11.4.2",
"version": "11.5.0",
"description": "Core components and util functions of React Flow.",
"keywords": [
"react",
@@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils';
import type { ConnectionLineComponent, HandleType, ReactFlowStore } from '../../types';
import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types';
import { Position, ConnectionLineType, ConnectionMode } from '../../types';
type ConnectionLineProps = {
connectionNodeId: string;
connectionHandleType: HandleType;
connectionLineType: ConnectionLineType;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
nodeId: string;
handleType: HandleType;
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
};
const oppositePosition = {
@@ -26,69 +25,62 @@ const oppositePosition = {
};
const ConnectionLine = ({
connectionNodeId,
connectionHandleType,
connectionLineStyle,
connectionLineType = ConnectionLineType.Bezier,
isConnectable,
CustomConnectionLineComponent,
nodeId,
handleType,
style,
type = ConnectionLineType.Bezier,
CustomComponent,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(connectionNodeId),
fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
[connectionNodeId]
[nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
let handleBounds = fromHandleBounds?.[connectionHandleType];
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds
? handleBounds
: fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source'];
handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
if (!fromNode || !isConnectable || !handleBounds) {
if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY;
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition) {
if (!fromPosition || !toPosition) {
return null;
}
const toPosition: Position = oppositePosition[fromPosition];
if (CustomConnectionLineComponent) {
if (CustomComponent) {
return (
<g className="react-flow__connection">
<CustomConnectionLineComponent
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
/>
</g>
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
/>
);
}
@@ -103,29 +95,62 @@ const ConnectionLine = ({
targetPosition: toPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) {
if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
} else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
} else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
} else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
return (
<g className="react-flow__connection">
<path d={dAttr} fill="none" className="react-flow__connection-path" style={connectionLineStyle} />
</g>
);
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
export default ConnectionLine;
type ConnectionLineWrapperProps = {
type: ConnectionLineType;
component?: ConnectionLineComponent;
containerStyle?: CSSProperties;
style?: CSSProperties;
};
const selector = (s: ReactFlowState) => ({
nodeId: s.connectionNodeId,
handleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
width: s.width,
height: s.height,
});
function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow);
const isValid = !!(nodeId && handleType && width && nodesConnectable);
if (!isValid) {
return null;
}
return (
<svg
style={containerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<g className="react-flow__connection">
<ConnectionLine nodeId={nodeId} handleType={handleType} style={style} type={type} CustomComponent={component} />
</g>
</svg>
);
}
export default ConnectionLineWrapper;
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useStore } from '../../hooks/useStore';
import { ReactFlowState } from '../../types';
@@ -1,6 +1,6 @@
import EdgeText from './EdgeText';
import type { BaseEdgeProps } from '../../types';
import { isNumeric } from '../../utils';
import type { BaseEdgeProps } from '../../types';
const BaseEdge = ({
path,
@@ -4,7 +4,7 @@ import cc from 'classcat';
import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { handleMouseDown } from '../Handle/handler';
import { handlePointerDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils';
@@ -99,14 +99,14 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent) => {
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
handleMouseDown({
handlePointerDown({
event,
handleId,
nodeId,
@@ -115,7 +115,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
getState: store.getState,
setState: store.setState,
isValidConnection,
elementEdgeUpdaterType: handleType,
edgeUpdaterType: handleType,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
});
};
+126 -137
View File
@@ -1,81 +1,20 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import { StoreApi } from 'zustand';
import { getHostForElement } from '../../utils';
import { ConnectionMode } from '../../types';
import type { OnConnect, Connection, HandleType, ReactFlowState } from '../../types';
import { getHostForElement, calcAutoPan, getEventPosition } from '../../utils';
import type { OnConnect, HandleType, ReactFlowState } from '../../types';
import { pointToRendererPoint, rendererPointToPoint } from '../../utils/graph';
import {
ConnectionHandle,
getClosestHandle,
getHandleLookup,
getHandleType,
isValidHandle,
resetRecentHandle,
ValidConnectionFunc,
} from './utils';
type ValidConnectionFunc = (connection: Connection) => boolean;
type Result = {
elementBelow: Element | null;
isValid: boolean;
connection: Connection;
isHoveringHandle: boolean;
};
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
export function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: string,
handleId: string | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
const result: Result = {
elementBelow,
isValid: false,
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
isHoveringHandle: false,
};
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
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
const isValid =
connectionMode === ConnectionMode.Strict
? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget)
: elementBelowNodeId !== nodeId || elementBelowHandleId !== handleId;
if (isValid) {
result.isValid = isValidConnection(connection);
}
}
return result;
}
function resetRecentHandle(hoveredHandle: Element): void {
hoveredHandle?.classList.remove('react-flow__handle-valid');
hoveredHandle?.classList.remove('react-flow__handle-connecting');
}
export function handleMouseDown({
export function handlePointerDown({
event,
handleId,
nodeId,
@@ -84,10 +23,10 @@ export function handleMouseDown({
getState,
setState,
isValidConnection,
elementEdgeUpdaterType,
edgeUpdaterType,
onEdgeUpdateEnd,
}: {
event: ReactMouseEvent;
event: ReactMouseEvent | ReactTouchEvent;
handleId: string | null;
nodeId: string;
onConnect: OnConnect;
@@ -95,35 +34,58 @@ export function handleMouseDown({
getState: StoreApi<ReactFlowState>['getState'];
setState: StoreApi<ReactFlowState>['setState'];
isValidConnection: ValidConnectionFunc;
elementEdgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent) => void;
edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
const {
connectionMode,
domNode,
autoPanOnConnect,
connectionRadius,
onConnectStart,
onConnectEnd,
panBy,
getNodes,
cancelConnection,
} = getState();
let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null;
if (!doc) {
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) {
return;
}
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source');
let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false;
if (!reactFlowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) {
return;
}
const handleLookup = getHandleLookup({
nodes: getNodes(),
nodeId,
handleId,
handleType,
});
const { onConnectStart, connectionMode } = getState();
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source';
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element;
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const autoPan = (): void => {
if (!autoPanOnConnect) {
return;
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan);
};
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionPosition,
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
@@ -131,68 +93,95 @@ export function handleMouseDown({
onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) {
function onPointerMove(event: MouseEvent | TouchEvent) {
const { transform } = getState();
connectionPosition = getEventPosition(event, containerBounds);
prevClosestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionPosition: prevClosestHandle
? rendererPointToPoint(
{
x: prevClosestHandle.x,
y: prevClosestHandle.y,
},
transform
)
: connectionPosition,
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
event,
if (!prevClosestHandle) {
return resetRecentHandle(prevActiveHandle);
}
const { connection, handleDomNode, isValid } = isValidHandle(
prevClosestHandle,
connectionMode,
isTarget,
nodeId,
handleId,
isTarget ? 'target' : 'source',
isValidConnection,
doc
);
if (!isHoveringHandle) {
return resetRecentHandle(recentHoveredHandle);
}
if (connection.source !== connection.target && elementBelow) {
resetRecentHandle(recentHoveredHandle);
recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid);
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode;
handleDomNode.classList.add('react-flow__handle-connecting');
handleDomNode.classList.toggle('react-flow__handle-valid', isValid);
}
}
function onMouseUp(event: MouseEvent) {
const { connection, isValid } = checkElementBelowIsValid(
event,
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
function onPointerUp(event: MouseEvent | TouchEvent) {
cancelAnimationFrame(autoPanId);
autoPanStarted = false;
if (isValid) {
onConnect?.(connection);
if (prevClosestHandle) {
const { connection, isValid } = isValidHandle(
prevClosestHandle,
connectionMode,
nodeId,
handleId,
isTarget ? 'target' : 'source',
isValidConnection,
doc
);
if (isValid) {
onConnect?.(connection);
}
}
getState().onConnectEnd?.(event);
onConnectEnd?.(event);
if (elementEdgeUpdaterType && onEdgeUpdateEnd) {
onEdgeUpdateEnd(event);
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(recentHoveredHandle);
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
resetRecentHandle(prevActiveHandle);
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
cancelConnection();
doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener);
doc.removeEventListener('touchmove', onPointerMove as EventListener);
doc.removeEventListener('touchend', onPointerUp as EventListener);
}
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
doc.addEventListener('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener);
}
+34 -13
View File
@@ -1,14 +1,16 @@
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react';
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import { checkElementBelowIsValid, handleMouseDown } from './handler';
import { getHostForElement } from '../../utils';
import { handlePointerDown } from './handler';
import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph';
import { Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types';
import { isValidHandle } from './utils';
import { errorMessages } from '../../contants';
const alwaysValid = () => true;
@@ -32,14 +34,20 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
children,
className,
onMouseDown,
onTouchStart,
...rest
},
ref
) => {
const store = useStoreApi();
const nodeId = useNodeId();
if (!nodeId) {
store.getState().onError?.('010', errorMessages['010']());
return null;
}
// @fixme: remove type assertion and handle nodeId === null
const nodeId = useNodeId() as string;
const { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow);
const handleId = id || null;
@@ -61,9 +69,11 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
onConnect?.(edgeParams);
};
const onMouseDownHandler = (event: ReactMouseEvent<HTMLDivElement>) => {
if (event.button === 0) {
handleMouseDown({
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({
event,
handleId,
nodeId,
@@ -74,7 +84,12 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isValidConnection,
});
}
onMouseDown?.(event);
if (isMouseTriggered) {
onMouseDown?.(event);
} else {
onTouchStart?.(event);
}
};
const onClick = (event: ReactMouseEvent) => {
@@ -86,12 +101,16 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
const { connection, isValid } = isValidHandle(
{
nodeId,
id: handleId,
type,
},
connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
connectionStartHandle.type,
isValidConnection,
doc
);
@@ -110,6 +129,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
@@ -126,7 +146,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionStartHandle?.type === type,
},
])}
onMouseDown={onMouseDownHandler}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
@@ -0,0 +1,154 @@
import { ConnectionMode } from '../../types';
import { internalsSymbol } from '../../utils';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types';
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
};
export type ValidConnectionFunc = (connection: Connection) => boolean;
// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
export function getHandles(
node: Node,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
res.push({
id: h.id || null,
type,
nodeId: node.id,
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
});
}
return res;
}, []);
}
export function getClosestHandle(
pos: XYPosition,
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandle: ConnectionHandle | null = null;
let minDistance = Infinity;
handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius && distance < minDistance) {
minDistance = distance;
closestHandle = handle;
}
});
return closestHandle;
}
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
};
// checks if and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle(
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'>,
connectionMode: ConnectionMode,
fromNodeId: string,
fromHandleId: string | null,
fromType: string,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
const result: Result = {
handleDomNode,
isValid: false,
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
};
if (handleDomNode) {
const handleIsTarget = handle.type === 'target';
const handleIsSource = handle.type === 'source';
const handleNodeId = handleDomNode.getAttribute('data-nodeid');
const handleId = handleDomNode.getAttribute('data-handleid');
const connection: Connection = {
source: isTarget ? handle.nodeId : fromNodeId,
sourceHandle: isTarget ? handle.id : fromHandleId,
target: isTarget ? fromNodeId : handle.nodeId,
targetHandle: isTarget ? fromHandleId : handle.id,
};
result.connection = connection;
// in strict mode we don't allow target to target or source to source connections
const isValid =
connectionMode === ConnectionMode.Strict
? (isTarget && handleIsSource) || (!isTarget && handleIsTarget)
: handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) {
result.isValid = isValidConnection(connection);
}
}
return result;
}
type GetHandleLookupParams = {
nodes: Node[];
nodeId: string;
handleId: string | null;
handleType: string;
};
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
return nodes.reduce<ConnectionHandle[]>((res, node) => {
if (node[internalsSymbol]) {
const { handleBounds } = node[internalsSymbol];
let sourceHandles: ConnectionHandle[] = [];
let targetHandles: ConnectionHandle[] = [];
if (handleBounds) {
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
}
res.push(...sourceHandles, ...targetHandles);
}
return res;
}, []);
}
export function getHandleType(
edgeUpdaterType: HandleType | undefined,
handleDomNode: Element | null
): HandleType | null {
if (edgeUpdaterType) {
return edgeUpdaterType;
} else if (handleDomNode?.classList.contains('target')) {
return 'target';
} else if (handleDomNode?.classList.contains('source')) {
return 'source';
}
return null;
}
export function resetRecentHandle(handleDomNode: Element): void {
handleDomNode?.classList.remove('react-flow__handle-valid');
handleDomNode?.classList.remove('react-flow__handle-connecting');
}
@@ -21,20 +21,18 @@ export interface NodesSelectionProps {
disableKeyboardA11y: boolean;
}
const selector = (s: ReactFlowState) => ({
transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`,
userSelectionActive: s.userSelectionActive,
});
const bboxSelector = (s: ReactFlowState) => {
const selector = (s: ReactFlowState) => {
const selectedNodes = s.getNodes().filter((n) => n.selected);
return getRectOfNodes(selectedNodes, s.nodeOrigin);
return {
...getRectOfNodes(selectedNodes, s.nodeOrigin),
transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`,
userSelectionActive: s.userSelectionActive,
};
};
function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) {
const store = useStoreApi();
const { transformString, userSelectionActive } = useStore(selector, shallow);
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const { width, height, x: left, y: top, transformString, userSelectionActive } = useStore(selector, shallow);
const updatePositions = useUpdateNodePositions();
const nodeRef = useRef<HTMLDivElement>(null);
@@ -45,6 +45,10 @@ type StoreUpdaterProps = Pick<
| 'noPanClassName'
| 'nodeOrigin'
| 'elevateNodesOnSelect'
| 'autoPanOnConnect'
| 'autoPanOnNodeDrag'
| 'onError'
| 'connectionRadius'
> & { rfId: string };
const selector = (s: ReactFlowState) => ({
@@ -119,6 +123,10 @@ const StoreUpdater = ({
noPanClassName,
nodeOrigin,
rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionRadius,
}: StoreUpdaterProps) => {
const {
setNodes,
@@ -172,6 +180,10 @@ const StoreUpdater = ({
useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState);
useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState);
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges);
@@ -10,9 +10,9 @@ const selector = (s: ReactFlowState) => ({
function UserSelection() {
const { userSelectionActive, userSelectionRect } = useStore(selector, shallow);
const showSelectionBox = userSelectionActive && userSelectionRect;
const isActive = userSelectionActive && userSelectionRect;
if (!showSelectionBox) {
if (!isActive) {
return null;
}
@@ -1,8 +1,9 @@
import { useMemo } from 'react';
import { devWarn } from '../../utils';
import { MarkerType } from '../../types';
import type { EdgeMarker } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
import { errorMessages } from '../../contants';
type SymbolProps = Omit<EdgeMarker, 'type'>;
@@ -38,17 +39,20 @@ export const MarkerSymbols = {
};
export function useMarkerSymbol(type: MarkerType) {
const store = useStoreApi();
const symbol = useMemo(() => {
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
if (!symbolExists) {
devWarn(`Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`);
store.getState().onError?.('009', errorMessages['009'](type));
return null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
@@ -1,27 +1,20 @@
import { memo } from 'react';
import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView';
import { devWarn } from '../../utils';
import { ConnectionMode, Position } from '../../types';
import type { Edge, ReactFlowState } from '../../types';
import { errorMessages } from '../../contants';
type EdgeRendererProps = Pick<
GraphViewProps,
| 'edgeTypes'
| 'connectionLineType'
| 'connectionLineType'
| 'connectionLineStyle'
| 'connectionLineComponent'
| 'connectionLineContainerStyle'
| 'connectionLineContainerStyle'
| 'onEdgeClick'
| 'onEdgeDoubleClick'
| 'defaultMarkerColor'
@@ -40,11 +33,10 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y'
> & {
elevateEdgesOnSelect: boolean;
children: ReactNode;
};
const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable,
@@ -52,35 +44,38 @@ const selector = (s: ReactFlowState) => ({
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
onError: s.onError,
});
const EdgeRenderer = (props: EdgeRendererProps) => {
const {
connectionNodeId,
connectionHandleType,
nodesConnectable,
edgesFocusable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
const EdgeRenderer = ({
defaultMarkerColor,
onlyRenderVisibleElements,
elevateEdgesOnSelect,
rfId,
edgeTypes,
noPanClassName,
onEdgeUpdate,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
onEdgeClick,
edgeUpdaterRadius,
onEdgeDoubleClick,
onEdgeUpdateStart,
onEdgeUpdateEnd,
children,
}: EdgeRendererProps) => {
const { edgesFocusable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = useStore(
selector,
shallow
);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
if (!width) {
return null;
}
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
@@ -91,11 +86,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={props.rfId} />}
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g>
{edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) {
return null;
@@ -103,32 +98,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
let edgeType = edge.type || 'default';
if (!props.edgeTypes[edgeType]) {
devWarn(
`Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
if (!edgeTypes[edgeType]) {
onError?.('011', errorMessages['011'](edgeType));
edgeType = 'default';
}
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
// when connection type is loose we can define all handles as sources
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) {
devWarn(
`Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: ${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
onError?.('008', errorMessages['008'](sourceHandle, edge));
return null;
}
@@ -146,7 +134,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, props.noPanClassName])}
className={cc([edge.className, noPanClassName])}
type={edgeType}
data={edge.data}
selected={!!edge.selected}
@@ -172,17 +160,17 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
onEdgeUpdate={props.onEdgeUpdate}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
onClick={props.onEdgeClick}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
rfId={props.rfId}
onEdgeUpdate={onEdgeUpdate}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
@@ -193,23 +181,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
</g>
</svg>
))}
{renderConnectionLine && (
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleType={connectionHandleType!}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
{children}
</>
);
};
@@ -72,21 +72,18 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: Ha
}
}
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) {
return null;
}
// there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one
let handle: HandleElement | null = null;
if (bounds.length === 1 || !handleId) {
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId)!;
if (handleId) {
return bounds.find((d) => d.id === handleId)!;
} else if (bounds.length === 1) {
return bounds[0];
}
return typeof handle === 'undefined' ? null : handle;
return null;
}
interface EdgePositions {
@@ -167,16 +164,15 @@ export function isEdgeVisible({
return overlappingArea > 0;
}
export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean] {
export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isInvalid =
!node ||
!handleBounds ||
!node.width ||
!node.height ||
typeof node.positionAbsolute?.x === 'undefined' ||
typeof node.positionAbsolute?.y === 'undefined';
const isValid =
handleBounds &&
node?.width &&
node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
@@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean
height: node?.height || 0,
},
handleBounds,
!isInvalid,
!!isValid,
];
}
@@ -5,6 +5,7 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import ViewportWrapper from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import ConnectionLine from '../../components/ConnectionLine';
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
export type GraphViewProps = Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> &
@@ -149,10 +150,6 @@ const GraphView = ({
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
@@ -167,7 +164,14 @@ const GraphView = ({
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId}
/>
>
<ConnectionLine
style={connectionLineStyle}
type={connectionLineType}
component={connectionLineComponent}
containerStyle={connectionLineContainerStyle}
/>
</EdgeRenderer>
<div className="react-flow__edgelabel-renderer" />
<NodeRenderer
@@ -4,12 +4,13 @@ import { shallow } from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../hooks/useStore';
import { clampPosition, devWarn, internalsSymbol } from '../../utils';
import { clampPosition, internalsSymbol } from '../../utils';
import { containerStyle } from '../../styles';
import { GraphViewProps } from '../GraphView';
import { getPositionWithOrigin } from './utils';
import { Position } from '../../types';
import type { ReactFlowState, WrapNodeProps } from '../../types';
import { errorMessages } from '../../contants';
type NodeRendererProps = Pick<
GraphViewProps,
@@ -36,13 +37,12 @@ const selector = (s: ReactFlowState) => ({
nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
onError: s.onError,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore(
selector,
shallow
);
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
@@ -78,9 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
devWarn(
`Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
onError?.('003', errorMessages['003'](nodeType));
nodeType = 'default';
}
@@ -5,7 +5,6 @@ import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
import { devWarn } from '../../utils';
import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
@@ -51,7 +50,6 @@ export const getPositionWithOrigin = ({
}
if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
devWarn('nodeOrigin must be between 0 and 1');
return { x, y };
}
+4 -10
View File
@@ -11,8 +11,9 @@ import { containerStyle } from '../../styles';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges } from '../../utils/changes';
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
import { getEventPosition } from '../../utils';
import { SelectionMode } from '../../types';
import type { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = {
isSelecting: boolean;
@@ -33,13 +34,6 @@ type PaneProps = {
>
>;
function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition {
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
};
}
const wrapHandler = (
handler: React.MouseEventHandler | undefined,
containerRef: React.MutableRefObject<HTMLDivElement | null>
@@ -118,7 +112,7 @@ const Pane = memo(
return;
}
const { x, y } = getMousePosition(event, containerBounds.current);
const { x, y } = getEventPosition(event, containerBounds.current);
resetSelectedElements();
@@ -145,7 +139,7 @@ const Pane = memo(
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
const mousePos = getMousePosition(event, containerBounds.current);
const mousePos = getEventPosition(event, containerBounds.current);
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
@@ -160,6 +160,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
elevateNodesOnSelect = true,
elevateEdgesOnSelect = false,
disableKeyboardA11y = false,
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
connectionRadius = 20,
onError,
style,
id,
...rest
@@ -287,6 +291,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
noPanClassName={noPanClassName}
nodeOrigin={nodeOrigin}
rfId={rfId}
autoPanOnConnect={autoPanOnConnect}
autoPanOnNodeDrag={autoPanOnNodeDrag}
onError={onError}
connectionRadius={connectionRadius}
/>
<SelectionListener onSelectionChange={onSelectionChange} />
{children}
@@ -5,6 +5,7 @@ import { devWarn } from '../../utils';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { errorMessages } from '../../contants';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
@@ -16,9 +17,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
devWarn(
"It looks like you have created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them. Help: https://reactflow.dev/error#200"
);
devWarn('002', errorMessages['002']());
}
typesKeysRef.current = typeKeys;
+20
View File
@@ -0,0 +1,20 @@
import { Edge, HandleElement } from './types';
export const errorMessages = {
'001': () =>
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001',
'002': () =>
"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",
'003': (nodeType: string) => `Node type "${nodeType}" not found. Using fallback type "default".`,
'004': () => 'The React Flow parent container needs a width and a height to render the graph.',
'005': () => 'Only child nodes can use a parent extent.',
'006': () => "Can't create edge. An edge needs a source and a target.",
'007': (id: string) => `The old edge with id=${id} does not exist.`,
'009': (type: string) => `Marker type "${type}" doesn't exist.`,
'008': (sourceHandle: HandleElement | null, edge: Edge) =>
`Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
}", edge id: ${edge.id}.`,
'010': () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
'011': (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
};
+100 -56
View File
@@ -7,7 +7,8 @@ import { useStoreApi } from '../../hooks/useStore';
import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
import useGetPointerPosition from '../useGetPointerPosition';
import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent } from '../../types';
import { calcAutoPan, getEventPosition } from '../../utils';
import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent, XYPosition } from '../../types';
export type UseDragData = { dx: number; dy: number };
@@ -34,10 +35,15 @@ function useDrag({
isSelectable,
selectNodesOnDrag,
}: UseDragParams) {
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi();
const dragItems = useRef<NodeDragItem[]>();
const [dragging, setDragging] = useState<boolean>(false);
const dragItems = useRef<NodeDragItem[]>([]);
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
const autoPanId = useRef(0);
const containerBounds = useRef<DOMRect | null>(null);
const mousePosition = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const dragEvent = useRef<MouseEvent | null>(null);
const autoPanStarted = useRef(false);
const getPointerPosition = useGetPointerPosition();
@@ -45,6 +51,80 @@ function useDrag({
if (nodeRef?.current) {
const selection = select(nodeRef.current);
const updateNodes = ({ x, y }: XYPosition) => {
const {
nodeInternals,
onNodeDrag,
onSelectionDrag,
updateNodePositions,
nodeExtent,
snapGrid,
snapToGrid,
nodeOrigin,
onError,
} = store.getState();
lastPos.current = { x, y };
let hasChange = false;
dragItems.current = dragItems.current.map((n) => {
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
if (snapToGrid) {
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin, onError);
// we want to make sure that we only fire a change event when there is a changes
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
return n;
});
if (!hasChange) {
return;
}
updateNodePositions(dragItems.current, true, true);
setDragging(true);
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
if (onDrag && dragEvent.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onDrag(dragEvent.current as MouseEvent, currentNode, nodes);
}
};
const autoPan = (): void => {
if (!containerBounds.current) {
return;
}
const [xMovement, yMovement] = calcAutoPan(mousePosition.current, containerBounds.current);
if (xMovement !== 0 || yMovement !== 0) {
const { transform, panBy } = store.getState();
lastPos.current.x = (lastPos.current.x ?? 0) - xMovement / transform[2];
lastPos.current.y = (lastPos.current.y ?? 0) - yMovement / transform[2];
updateNodes(lastPos.current as XYPosition);
panBy({ x: xMovement, y: yMovement });
}
autoPanId.current = requestAnimationFrame(autoPan);
};
if (disabled) {
selection.on('.drag', null);
} else {
@@ -56,6 +136,7 @@ function useDrag({
unselectNodesAndEdges,
onNodeDragStart,
onSelectionDragStart,
domNode,
} = store.getState();
const onStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
@@ -86,72 +167,35 @@ function useDrag({
});
onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
}
containerBounds.current = domNode?.getBoundingClientRect() || null;
mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
})
.on('drag', (event: UseDragEvent) => {
const {
updateNodePositions,
nodeInternals,
nodeExtent,
onNodeDrag,
onSelectionDrag,
snapGrid,
snapToGrid,
nodeOrigin,
} = store.getState();
const pointerPos = getPointerPosition(event);
const { autoPanOnNodeDrag } = store.getState();
if (!autoPanStarted.current && autoPanOnNodeDrag) {
autoPanStarted.current = true;
autoPan();
}
// skip events without movement
if (
(lastPos.current.x !== pointerPos.xSnapped || lastPos.current.y !== pointerPos.ySnapped) &&
dragItems.current
) {
lastPos.current = {
x: pointerPos.xSnapped,
y: pointerPos.ySnapped,
};
dragEvent.current = event.sourceEvent as MouseEvent;
mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
let hasChange = false;
dragItems.current = dragItems.current.map((n) => {
const nextPosition = { x: pointerPos.x - n.distance.x, y: pointerPos.y - n.distance.y };
if (snapToGrid) {
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin);
// we want to make sure that we only fire a change event when there is a changes
hasChange =
hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
return n;
});
if (!hasChange) {
return;
}
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
updateNodePositions(dragItems.current, true, true);
setDragging(true);
if (onDrag) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onDrag(event.sourceEvent as MouseEvent, currentNode, nodes);
}
updateNodes(pointerPos);
}
})
.on('end', (event: UseDragEvent) => {
setDragging(false);
autoPanStarted.current = false;
cancelAnimationFrame(autoPanId.current);
if (dragItems.current) {
const { updateNodePositions, nodeInternals, onNodeDragStop, onSelectionDragStop } = store.getState();
const onStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
+6 -4
View File
@@ -1,8 +1,9 @@
import type { RefObject } from 'react';
import { clampPosition, devWarn, isNumeric } from '../../utils';
import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types';
import { clampPosition, isNumeric } from '../../utils';
import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, OnError, XYPosition } from '../../types';
import { getNodePositionWithOrigin } from '../../utils/graph';
import { errorMessages } from '../../contants';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
@@ -62,7 +63,8 @@ export function calcNextPosition(
nextPosition: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0]
nodeOrigin: NodeOrigin = [0, 0],
onError?: OnError
): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent;
@@ -81,7 +83,7 @@ export function calcNextPosition(
]
: currentExtent;
} else {
devWarn('Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
onError?.('005', errorMessages['005']());
currentExtent = nodeExtent;
}
+3 -4
View File
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
import type { MutableRefObject } from 'react';
import { useStoreApi } from '../hooks/useStore';
import { devWarn, getDimensions } from '../utils';
import { getDimensions } from '../utils';
import { errorMessages } from '../contants';
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi();
@@ -18,9 +19,7 @@ function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>)
const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) {
devWarn(
'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
);
store.getState().onError?.('004', errorMessages['004']());
}
store.setState({ width: size.width || 500, height: size.height || 500 });
+4 -4
View File
@@ -3,10 +3,10 @@ import { useStore as useZustandStore } from 'zustand';
import type { StoreApi } from 'zustand';
import StoreContext from '../contexts/RFStoreContext';
import { errorMessages } from '../contants';
import type { ReactFlowState } from '../types';
const errorMessage =
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#100';
const zustandErrorMessage = errorMessages['001']();
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
@@ -17,7 +17,7 @@ function useStore<StateSlice = ExtractState>(
const store = useContext(StoreContext);
if (store === null) {
throw new Error(errorMessage);
throw new Error(zustandErrorMessage);
}
return useZustandStore(store, selector, equalityFn);
@@ -27,7 +27,7 @@ const useStoreApi = () => {
const store = useContext(StoreContext);
if (store === null) {
throw new Error(errorMessage);
throw new Error(zustandErrorMessage);
}
return useMemo(
@@ -7,7 +7,8 @@ function useUpdateNodePositions() {
const store = useStoreApi();
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid } = store.getState();
const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid, onError } =
store.getState();
const selectedNodes = getNodes().filter((n) => n.selected);
// by default a node moves 5px on each key press, or 20px if shift is pressed
// if snap grid is enabled, we use that for the velocity.
@@ -27,10 +28,17 @@ function useUpdateNodePositions() {
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent);
const { positionAbsolute, position } = calcNextPosition(
n,
nextPosition,
nodeInternals,
nodeExtent,
undefined,
onError
);
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
n.position = position;
n.positionAbsolute = positionAbsolute;
}
return n;
+20
View File
@@ -1,4 +1,5 @@
import { createStore } from 'zustand';
import { zoomIdentity } from 'd3-zoom';
import { clampPosition, getDimensions, internalsSymbol } from '../utils';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -18,6 +19,7 @@ import type {
NodeDragItem,
UnselectNodesAndEdgesParams,
NodeChange,
XYPosition,
} from '../types';
const createRFStore = () =>
@@ -250,10 +252,28 @@ const createRFStore = () =>
nodeInternals: new Map(nodeInternals),
});
},
panBy: (delta: XYPosition) => {
const { transform, width, height, d3Zoom, d3Selection, translateExtent } = get();
if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) {
return;
}
const nextTransform = zoomIdentity.translate(transform[0] + delta.x, transform[1] + delta.y).scale(transform[2]);
const extent: CoordinateExtent = [
[0, 0],
[width, height],
];
const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, translateExtent);
d3Zoom.transform(d3Selection, constrainedTransform);
},
cancelConnection: () =>
set({
connectionNodeId: initialState.connectionNodeId,
connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
}),
reset: () => set({ ...initialState }),
}));
+5
View File
@@ -1,3 +1,4 @@
import { devWarn } from '../utils';
import { ConnectionMode } from '../types';
import type { CoordinateExtent, ReactFlowStore } from '../types';
@@ -56,6 +57,10 @@ const initialState: ReactFlowStore = {
connectOnClick: true,
ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
connectionRadius: 20,
onError: devWarn,
};
export default initialState;
+1 -1
View File
@@ -98,7 +98,7 @@
.react-flow__connection {
pointer-events: none;
&.animated {
.animated {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
+6 -1
View File
@@ -36,6 +36,7 @@ import type {
EdgeMouseHandler,
HandleType,
SelectionMode,
OnError,
} from '.';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
@@ -61,7 +62,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
onEdgeMouseLeave?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
onNodesDelete?: OnNodesDelete;
@@ -139,6 +140,10 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
elevateNodesOnSelect?: boolean;
elevateEdgesOnSelect?: boolean;
disableKeyboardA11y?: boolean;
autoPanOnNodeDrag?: boolean;
autoPanOnConnect?: boolean;
connectionRadius?: number;
onError?: OnError;
};
export type ReactFlowRefType = HTMLDivElement;
+1 -1
View File
@@ -85,7 +85,7 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onMouseLeave?: EdgeMouseHandler;
edgeUpdaterRadius?: number;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
rfId?: string;
isFocusable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
+15 -3
View File
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } from 'react';
import type {
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
ComponentType,
MemoExoticComponent,
} from 'react';
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
@@ -77,8 +82,8 @@ export type OnConnectStartParams = {
handleType: HandleType | null;
};
export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent) => void;
export type OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type Viewport = {
x: number;
@@ -201,6 +206,7 @@ export type ReactFlowStore = {
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onError?: OnError;
// event handlers
onViewportChangeStart?: OnViewportChange;
@@ -210,6 +216,9 @@ export type ReactFlowStore = {
onSelectionChange?: OnSelectionChangeFunc;
ariaLiveMessage: string;
autoPanOnConnect: boolean;
autoPanOnNodeDrag: boolean;
connectionRadius: number;
};
export type ReactFlowActions = {
@@ -230,6 +239,7 @@ export type ReactFlowActions = {
cancelConnection: () => void;
reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void;
panBy: (delta: XYPosition) => void;
};
export type ReactFlowState = ReactFlowStore & ReactFlowActions;
@@ -261,3 +271,5 @@ export type SelectionRect = Rect & {
startX: number;
startY: number;
};
export type OnError = (id: string, message: string) => void;
+2 -2
View File
@@ -5,11 +5,11 @@ import { internalsSymbol } from '../utils';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
// interface for the user node items
export type Node<T = any> = {
export type Node<T = any, U extends string | undefined = string | undefined> = {
id: string;
position: XYPosition;
data: T;
type?: string;
type?: U;
style?: CSSProperties;
className?: string;
sourcePosition?: Position;
+11 -3
View File
@@ -13,6 +13,7 @@ import {
NodeInternals,
NodeOrigin,
} from '../types';
import { errorMessages } from '../contants';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element;
@@ -70,7 +71,7 @@ const connectionExists = (edge: Edge, edges: Edge[]) => {
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) {
devWarn("Can't create edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600");
devWarn('006', errorMessages['006']());
return edges;
}
@@ -94,7 +95,7 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] =>
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) {
devWarn("Can't create a new edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600");
devWarn('006', errorMessages['006']());
return edges;
}
@@ -102,7 +103,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) {
devWarn(`The old edge with id=${oldEdge.id} does not exist. Help: https://reactflow.dev/error#700`);
devWarn('007', errorMessages['007'](oldEdge.id));
return edges;
}
@@ -141,6 +142,13 @@ export const pointToRendererPoint = (
return position;
};
export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => {
return {
x: x * tScale + tx,
y: y * tScale + ty,
};
};
export const getNodePositionWithOrigin = (
node: Node | undefined,
nodeOrigin: NodeOrigin = [0, 0]
+45 -3
View File
@@ -1,4 +1,9 @@
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
import type {
KeyboardEvent as ReactKeyboardEvent,
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
} from 'react';
import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
@@ -13,6 +18,25 @@ export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: Coo
y: clamp(position.y, extent[0][1], extent[1][1]),
});
// returns a number between 0 and 1 that represents the velocity of the movement
// when the mouse is close to the edge of the canvas
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
if (value < min) {
return clamp(Math.abs(value - min), 1, 50) / 50;
} else if (value > max) {
return -clamp(Math.abs(value - max), 1, 50) / 50;
}
return 0;
};
export const calcAutoPan = (pos: XYPosition, bounds: Dimensions): number[] => {
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
return [xMovement, yMovement];
};
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
@@ -65,9 +89,9 @@ export const internalsSymbol = Symbol.for('internals');
// used for a11y key board controls for nodes and edges
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
export const devWarn = (message: string) => {
export const devWarn = (id: string, message: string) => {
if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: ${message}`);
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
}
};
@@ -91,3 +115,21 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole
!!target?.closest('.nokey')
);
}
export const isMouseEvent = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent
): event is MouseEvent | ReactMouseEvent => 'clientX' in event;
export const getEventPosition = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent,
bounds?: DOMRect
) => {
const isMouseTriggered = isMouseEvent(event);
const evtX = isMouseTriggered ? event.clientX : event.touches?.[0].clientX;
const evtY = isMouseTriggered ? event.clientY : event.touches?.[0].clientY;
return {
x: evtX - (bounds?.left ?? 0),
y: evtY - (bounds?.top ?? 0),
};
};
+7
View File
@@ -1,5 +1,12 @@
# @reactflow/minimap
## 11.3.3
### Patch Changes
- Updated dependencies [[`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694), [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52), [`4c516882`](https://github.com/wbkd/react-flow/commit/4c516882d2bbf426c1832a53ad40763cc1abef92)]:
- @reactflow/core@11.5.0
## 11.3.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@reactflow/minimap",
"version": "11.3.2",
"version": "11.3.3",
"description": "Minimap component for React Flow.",
"keywords": [
"react",
+11
View File
@@ -1,5 +1,16 @@
# @reactflow/node-resizer
## 2.0.0
### Major Changes
- [#2749](https://github.com/wbkd/react-flow/pull/2749) [`e347dd82`](https://github.com/wbkd/react-flow/commit/e347dd82d342bf9c4884ca667afaa5cf639283e5) - Add `shouldResize`, rename and cleanup types - `ResizeEventParams` is now `ResizeParams`
### Patch Changes
- Updated dependencies [[`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694), [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52), [`4c516882`](https://github.com/wbkd/react-flow/commit/4c516882d2bbf426c1832a53ad40763cc1abef92)]:
- @reactflow/core@11.5.0
## 1.2.2
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@reactflow/node-resizer",
"version": "1.2.2",
"version": "2.0.0",
"description": "A helper component for resizing nodes.",
"keywords": [
"react",
@@ -38,7 +38,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@reactflow/core": "workspace:*",
"@reactflow/core": "workspace:^11.3.3",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
@@ -14,6 +14,7 @@ export default function NodeResizer({
color,
minWidth = 10,
minHeight = 10,
shouldResize,
onResizeStart,
onResize,
onResizeEnd,
@@ -36,6 +37,7 @@ export default function NodeResizer({
minWidth={minWidth}
minHeight={minHeight}
onResizeStart={onResizeStart}
shouldResize={shouldResize}
onResize={onResize}
onResizeEnd={onResizeEnd}
/>
@@ -51,6 +53,7 @@ export default function NodeResizer({
minWidth={minWidth}
minHeight={minHeight}
onResizeStart={onResizeStart}
shouldResize={shouldResize}
onResize={onResize}
onResizeEnd={onResizeEnd}
/>
+24 -1
View File
@@ -12,6 +12,7 @@ import {
} from '@reactflow/core';
import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types';
import { getDirection } from './utils';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
@@ -31,6 +32,7 @@ function ResizeControl({
color,
minWidth = 10,
minHeight = 10,
shouldResize,
onResizeStart,
onResize,
onResizeEnd,
@@ -140,7 +142,28 @@ function ResizeControl({
prevValues.current.height = height;
}
onResize?.(event, { ...prevValues.current });
if (changes.length === 0) {
return;
}
const direction = getDirection({
width: prevValues.current.width,
prevWidth,
height: prevValues.current.height,
prevHeight,
invertX,
invertY,
});
const nextValues = { ...prevValues.current, direction };
const callResize = shouldResize?.(event, nextValues);
if (callResize === false) {
return;
}
onResize?.(event, nextValues);
triggerNodeChanges(changes);
}
})
+17 -5
View File
@@ -1,13 +1,24 @@
import type { CSSProperties, ReactNode } from 'react';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
export type ResizeEventParams = {
export type ResizeParams = {
x: number;
y: number;
width: number;
height: number;
};
export type ResizeParamsWithDirection = ResizeParams & {
direction: number[];
};
type OnResizeHandler<Params = ResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
export type ShouldResize = OnResizeHandler<ResizeParamsWithDirection, boolean>;
export type OnResizeStart = OnResizeHandler;
export type OnResize = OnResizeHandler<ResizeParamsWithDirection>;
export type OnResizeEnd = OnResizeHandler;
export type NodeResizerProps = {
nodeId?: string;
color?: string;
@@ -18,9 +29,10 @@ export type NodeResizerProps = {
isVisible?: boolean;
minWidth?: number;
minHeight?: number;
onResizeStart?: (event: ResizeDragEvent, params: ResizeEventParams) => void;
onResize?: (event: ResizeDragEvent, params: ResizeEventParams) => void;
onResizeEnd?: (event: ResizeDragEvent, params: ResizeEventParams) => void;
shouldResize?: ShouldResize;
onResizeStart?: OnResizeStart;
onResize?: OnResize;
onResizeEnd?: OnResizeEnd;
};
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
@@ -34,7 +46,7 @@ export enum ResizeControlVariant {
export type ResizeControlProps = Pick<
NodeResizerProps,
'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'onResizeStart' | 'onResize' | 'onResizeEnd'
'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'shouldResize' | 'onResizeStart' | 'onResize' | 'onResizeEnd'
> & {
position?: ControlPosition;
variant?: ResizeControlVariant;
+26
View File
@@ -0,0 +1,26 @@
type GetDirectionParams = {
width: number;
prevWidth: number;
height: number;
prevHeight: number;
invertX: boolean;
invertY: boolean;
};
// returns an array of two numbers (0, 1 or -1) representing the direction of the resize
// 0 = no change, 1 = increase, -1 = decrease
export function getDirection({ width, prevWidth, height, prevHeight, invertX, invertY }: GetDirectionParams) {
const deltaWidth = width - prevWidth;
const deltaHeight = height - prevHeight;
const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0];
if (deltaWidth && invertX) {
direction[0] = direction[0] * -1;
}
if (deltaHeight && invertY) {
direction[1] = direction[1] * -1;
}
return direction;
}
+1 -1
View File
@@ -36,7 +36,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@reactflow/core": "workspace:*",
"@reactflow/core": "workspace:^11.3.3",
"classcat": "^5.0.3",
"zustand": "^4.3.1"
},
+30
View File
@@ -1,5 +1,35 @@
# reactflow
## 11.5.0
Lot's of improvements are coming with this release!
- **Connecting radius**: No need to drop a connection line on top of handle anymore. You only need to be close to the handle. That radius can be configured with the `connectionRadius` prop.
- **Auto pan**: When you drag a node, a selection or the connection line to the border of the pane, it will pan into that direction. That makes it easier to connect far away nodes for example. If you don't like it you can set `autoPnaOnNodeDrag` and `autoPanOnConnect` to false.
- **Touch devices**: It's finally possibleto connect nodes with the connection line on touch devices. In combination with the new auto pan and connection radius the overall UX is way better.
- **Errors**: We added an `onError` prop to get notified when an error like "couldn't find source handle" happens. This is useful if you want to log errors for example.
- **Node type**: We added a second param to the generic `Node` type. You can not only pass `NodeData` but also the type as a second param:
```ts
type MyCustomNode = Node<MyCustomNodeData, 'custom-node-type'>
```
This makes it easier to work with different custom nodes and data types.
### Minor Changes
- [#2754](https://github.com/wbkd/react-flow/pull/2754) [`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694) - Add auto pan for connecting and node dragging and `connectionRadius`
- [#2773](https://github.com/wbkd/react-flow/pull/2773) - Add `onError` prop to get notified when an error happens
### Patch Changes
- [#2763](https://github.com/wbkd/react-flow/pull/2763) [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52) - Connecting nodes: Enable connections on touch devices
- [#2620](https://github.com/wbkd/react-flow/pull/2620) - Thanks [RichSchulz](https://github.com/RichSchulz)! - Types: improve typing for node type
- Updated dependencies [[`e96309b6`](https://github.com/wbkd/react-flow/commit/e96309b6a57b1071faeebf7b0547fef7fd418694), [`85003b01`](https://github.com/wbkd/react-flow/commit/85003b01add71ea852bd5b0d2f1e7496050a6b52), [`4c516882`](https://github.com/wbkd/react-flow/commit/4c516882d2bbf426c1832a53ad40763cc1abef92)]:
- @reactflow/core@11.5.0
- @reactflow/background@11.1.3
- @reactflow/controls@11.1.3
- @reactflow/minimap@11.3.3
## 11.4.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "reactflow",
"version": "11.4.2",
"version": "11.5.0",
"description": "A highly customizable React library for building node-based editors and interactive flow charts",
"keywords": [
"react",
+56 -56
View File
@@ -32,15 +32,15 @@ importers:
'@changesets/changelog-github': registry.npmjs.org/@changesets/changelog-github/0.4.7
'@changesets/cli': registry.npmjs.org/@changesets/cli/2.25.0
'@preconstruct/cli': registry.npmjs.org/@preconstruct/cli/2.2.1
'@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.48.1_fsnwvyoekxjd6kngphv4egujga
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.48.1_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0_equgvmdpavr6nminvx2krii6h4
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.49.0_id2eilsndvzhjjktb64trvy3gu
autoprefixer: registry.npmjs.org/autoprefixer/10.4.9_postcss@8.4.21
concurrently: registry.npmjs.org/concurrently/7.6.0
cypress: registry.npmjs.org/cypress/10.7.0
eslint: registry.npmjs.org/eslint/8.23.1
eslint-config-prettier: registry.npmjs.org/eslint-config-prettier/8.5.0_eslint@8.23.1
eslint-plugin-prettier: registry.npmjs.org/eslint-plugin-prettier/4.2.1_cabrci5exjdaojcvd6xoxgeowu
eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.32.0_eslint@8.23.1
eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.32.1_eslint@8.23.1
postcss: registry.npmjs.org/postcss/8.4.21
postcss-cli: registry.npmjs.org/postcss-cli/10.1.0_postcss@8.4.21
postcss-combine-duplicated-selectors: registry.npmjs.org/postcss-combine-duplicated-selectors/10.0.3_postcss@8.4.21
@@ -213,7 +213,7 @@ importers:
packages/node-resizer:
specifiers:
'@reactflow/core': workspace:*
'@reactflow/core': workspace:^11.3.3
'@reactflow/eslint-config': workspace:*
'@reactflow/rollup-config': workspace:*
'@reactflow/tsconfig': workspace:*
@@ -248,7 +248,7 @@ importers:
packages/node-toolbar:
specifiers:
'@reactflow/core': workspace:*
'@reactflow/core': workspace:^11.3.3
'@reactflow/eslint-config': workspace:*
'@reactflow/rollup-config': workspace:*
'@reactflow/tsconfig': workspace:*
@@ -312,7 +312,7 @@ importers:
eslint: registry.npmjs.org/eslint/8.23.1
eslint-config-prettier: registry.npmjs.org/eslint-config-prettier/8.5.0_eslint@8.23.1
eslint-config-turbo: registry.npmjs.org/eslint-config-turbo/0.0.7_eslint@8.23.1
eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.32.0_eslint@8.23.1
eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.32.1_eslint@8.23.1
tooling/rollup-config:
specifiers:
@@ -2186,11 +2186,11 @@ packages:
dev: true
optional: true
registry.npmjs.org/@typescript-eslint/eslint-plugin/5.48.1_fsnwvyoekxjd6kngphv4egujga:
resolution: {integrity: sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz}
id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.48.1
registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0_equgvmdpavr6nminvx2krii6h4:
resolution: {integrity: sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz}
id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0
name: '@typescript-eslint/eslint-plugin'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
'@typescript-eslint/parser': ^5.0.0
@@ -2200,10 +2200,10 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.48.1_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1
'@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.48.1_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.48.1_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.49.0_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0
'@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.49.0_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu
debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1
ignore: registry.npmjs.org/ignore/5.2.4
@@ -2216,11 +2216,11 @@ packages:
- supports-color
dev: true
registry.npmjs.org/@typescript-eslint/parser/5.48.1_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.1.tgz}
id: registry.npmjs.org/@typescript-eslint/parser/5.48.1
registry.npmjs.org/@typescript-eslint/parser/5.49.0_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz}
id: registry.npmjs.org/@typescript-eslint/parser/5.49.0
name: '@typescript-eslint/parser'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -2229,9 +2229,9 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0_typescript@4.9.4
debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1
typescript: registry.npmjs.org/typescript/4.9.4
@@ -2239,21 +2239,21 @@ packages:
- supports-color
dev: true
registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1:
resolution: {integrity: sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz}
registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0:
resolution: {integrity: sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz}
name: '@typescript-eslint/scope-manager'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0
dev: true
registry.npmjs.org/@typescript-eslint/type-utils/5.48.1_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz}
id: registry.npmjs.org/@typescript-eslint/type-utils/5.48.1
registry.npmjs.org/@typescript-eslint/type-utils/5.49.0_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz}
id: registry.npmjs.org/@typescript-eslint/type-utils/5.49.0
name: '@typescript-eslint/type-utils'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@@ -2262,8 +2262,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4
'@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.48.1_id2eilsndvzhjjktb64trvy3gu
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0_typescript@4.9.4
'@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu
debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1
tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.9.4
@@ -2272,18 +2272,18 @@ packages:
- supports-color
dev: true
registry.npmjs.org/@typescript-eslint/types/5.48.1:
resolution: {integrity: sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz}
registry.npmjs.org/@typescript-eslint/types/5.49.0:
resolution: {integrity: sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz}
name: '@typescript-eslint/types'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4:
resolution: {integrity: sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz}
id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1
registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0_typescript@4.9.4:
resolution: {integrity: sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz}
id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0
name: '@typescript-eslint/typescript-estree'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
@@ -2291,8 +2291,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0
debug: registry.npmjs.org/debug/4.3.4
globby: registry.npmjs.org/globby/11.1.0
is-glob: registry.npmjs.org/is-glob/4.0.3
@@ -2303,20 +2303,20 @@ packages:
- supports-color
dev: true
registry.npmjs.org/@typescript-eslint/utils/5.48.1_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.1.tgz}
id: registry.npmjs.org/@typescript-eslint/utils/5.48.1
registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz}
id: registry.npmjs.org/@typescript-eslint/utils/5.49.0
name: '@typescript-eslint/utils'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@types/json-schema': registry.npmjs.org/@types/json-schema/7.0.11
'@types/semver': registry.npmjs.org/@types/semver/7.3.13
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0_typescript@4.9.4
eslint: registry.npmjs.org/eslint/8.23.1
eslint-scope: registry.npmjs.org/eslint-scope/5.1.1
eslint-utils: registry.npmjs.org/eslint-utils/3.0.0_eslint@8.23.1
@@ -2326,13 +2326,13 @@ packages:
- typescript
dev: true
registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1:
resolution: {integrity: sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz}
registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0:
resolution: {integrity: sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz}
name: '@typescript-eslint/visitor-keys'
version: 5.48.1
version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys/3.3.0
dev: true
@@ -3723,11 +3723,11 @@ packages:
prettier-linter-helpers: registry.npmjs.org/prettier-linter-helpers/1.0.0
dev: true
registry.npmjs.org/eslint-plugin-react/7.32.0_eslint@8.23.1:
resolution: {integrity: sha512-vSBi1+SrPiLZCGvxpiZIa28fMEUaMjXtCplrvxcIxGzmFiYdsXQDwInEjuv5/i/2CTTxbkS87tE8lsQ0Qxinbw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.0.tgz}
id: registry.npmjs.org/eslint-plugin-react/7.32.0
registry.npmjs.org/eslint-plugin-react/7.32.1_eslint@8.23.1:
resolution: {integrity: sha512-vOjdgyd0ZHBXNsmvU+785xY8Bfe57EFbTYYk8XrROzWpr9QBvpjITvAXt9xqcE6+8cjR/g1+mfumPToxsl1www==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.1.tgz}
id: registry.npmjs.org/eslint-plugin-react/7.32.1
name: eslint-plugin-react
version: 7.32.0
version: 7.32.1
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8