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 }) .type('{shift}', { release: false })
.wait(50) .wait(50)
.get('.react-flow__pane') .get('.react-flow__pane')
.trigger('mousedown', 1000, 50, { button: 0, force: true }) .trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1, 400, { button: 0 }) .trigger('mousemove', 1000, 450, { button: 0 })
.wait(50) .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(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'); cy.get('.react-flow__nodesselection-rect');
@@ -74,11 +74,13 @@ describe('Basic Flow Rendering', () => {
.trigger('mousedown', 'topRight', { button: 0, force: true }) .trigger('mousedown', 'topRight', { button: 0, force: true })
.trigger('mousemove', 'bottomLeft', { button: 0 }) .trigger('mousemove', 'bottomLeft', { button: 0 })
.wait(50) .wait(50)
.trigger('mouseup', 'bottomLeft', { force: true }) .trigger('mouseup', 'bottomLeft', { button: 0, force: true })
.wait(50) .wait(400)
.get('.react-flow__node') .get('.react-flow__node')
.should('have.class', 'selected') .should('have.class', 'selected');
.get('.react-flow__nodesselection-rect');
cy.wait(200);
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true }); cy.get('body').type('{shift}', { release: true });
}); });
@@ -113,14 +115,14 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 3') .contains('Node 3')
.find('.react-flow__handle.source') .find('.react-flow__handle.source')
.trigger('mousedown', { button: 0 }); .trigger('mousedown', { force: true, button: 0 });
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 4') .contains('Node 4')
.find('.react-flow__handle.target') .find('.react-flow__handle.target')
.trigger('mousemove', { force: true }) .trigger('mousemove', { force: true, button: 0 })
.wait(50) .wait(200)
.trigger('mouseup', { force: true }); .trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').should('have.length', 3); cy.get('.react-flow__edge').should('have.length', 3);
}); });
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000 --open", "dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000", "serve": "vite serve --port 3000",
"build": "vite build", "build": "vite build",
"test:dev": "cypress open", "test:dev": "cypress open",
@@ -48,8 +48,6 @@ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const defaultEdgeOptions = {}; const defaultEdgeOptions = {};
const BasicFlow = () => { const BasicFlow = () => {
@@ -96,7 +94,6 @@ const BasicFlow = () => {
selectNodesOnDrag={false} selectNodesOnDrag={false}
elevateEdgesOnSelect elevateEdgesOnSelect
elevateNodesOnSelect={false} elevateNodesOnSelect={false}
// nodeOrigin={nodeOrigin}
> >
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,9 +1,8 @@
import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'reactflow'; import { ConnectionLineComponentProps } from 'reactflow';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => { function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
return ( return (
<g> <>
<path <path
fill="none" fill="none"
stroke="#222" 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}`} 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} /> <circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g> </>
); );
}; }
export default ConnectionLine; 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 { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'reactflow';
import { getEdgeParams } from './utils'; 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 { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const d = getBezierPath({ const [path] = getBezierPath({
sourceX: sx, sourceX: sx,
sourceY: sy, sourceY: sy,
sourcePosition: sourcePos, sourcePosition: sourcePos,
@@ -28,7 +28,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
return ( return (
<g className="react-flow__connection"> <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> </g>
); );
}; };
@@ -1,7 +1,7 @@
import { memo, FC, CSSProperties } from 'react'; import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow'; 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 '@reactflow/node-resizer/dist/style.css';
import ResizeIcon from './ResizeIcon'; import ResizeIcon from './ResizeIcon';
@@ -1,14 +1,14 @@
import { memo, FC } from 'react'; import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow'; 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 '@reactflow/node-resizer/dist/style.css';
const CustomNode: FC<NodeProps> = ({ id, data }) => { const CustomNode: FC<NodeProps> = ({ id, data }) => {
return ( return (
<> <>
<NodeResizeControl color="red" position="top" /> <NodeResizeControl color="red" position={Position.Top} />
<NodeResizeControl color="red" position="bottom" /> <NodeResizeControl color="red" position={Position.Bottom} />
<Handle type="target" position={Position.Left} /> <Handle type="target" position={Position.Left} />
<div style={{ padding: 10 }}>{data.label}</div> <div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Right} /> <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 { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow'; 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'; import '@reactflow/node-resizer/dist/style.css';
const onResizeStart = (_: ResizeDragEvent, params: ResizeEventParams) => { const onResizeStart: OnResizeStart = (_, params) => {
console.log('resize start', params); console.log('resize start', params);
}; };
const onResize = (_: ResizeDragEvent, params: ResizeEventParams) => { const onResize: OnResize = (_, params) => {
console.log('resize', params); console.log('resize', params);
}; };
const onResizeEnd = (_: ResizeDragEvent, params: ResizeEventParams) => { const onResizeEnd: OnResizeEnd = (_, params) => {
console.log('resize end', params); console.log('resize end', params);
}; };
const shouldResize: ShouldResize = (_, params) => {
console.log('should resize', params);
return true;
};
const CustomNode: FC<NodeProps> = ({ data, selected }) => { const CustomNode: FC<NodeProps> = ({ data, selected }) => {
return ( return (
<> <>
@@ -23,6 +29,7 @@ const CustomNode: FC<NodeProps> = ({ data, selected }) => {
minWidth={100} minWidth={100}
minHeight={100} minHeight={100}
isVisible={selected} isVisible={selected}
shouldResize={shouldResize}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
@@ -4,11 +4,13 @@ import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useE
import NodeResizerNode from './NodeResizerNode'; import NodeResizerNode from './NodeResizerNode';
import CustomResizer from './CustomResizer'; import CustomResizer from './CustomResizer';
import CustomResizer2 from './CustomResizer2'; import CustomResizer2 from './CustomResizer2';
import CustomResizer3 from './CustomResizer3';
const nodeTypes = { const nodeTypes = {
resizer: NodeResizerNode, resizer: NodeResizerNode,
customResizer: CustomResizer, customResizer: CustomResizer,
customResizer2: CustomResizer2, customResizer2: CustomResizer2,
customResizer3: CustomResizer3,
}; };
const initialEdges = [ const initialEdges = [
@@ -60,6 +62,13 @@ const initialNodes = [
position: { x: 250, y: 250 }, position: { x: 250, y: 250 },
style: { border: '1px solid #222', fontSize: 10 }, 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 = () => { const CustomNodeFlow = () => {
@@ -87,7 +96,7 @@ const CustomNodeFlow = () => {
> >
<Controls /> <Controls />
<Panel position="bottom-right"> <Panel position="bottom-right">
<button onClick={() => setSnapToGrid(!snapToGrid)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button> <button onClick={() => setSnapToGrid((s) => !s)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
@@ -12,6 +12,7 @@ import ReactFlow, {
MiniMap, MiniMap,
Background, Background,
Panel, Panel,
NodeOrigin,
} from 'reactflow'; } from 'reactflow';
import DebugNode from './DebugNode'; 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'; import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
@@ -35,7 +35,7 @@ const nodesA: Node[] = [
]; ];
const edgesA: Edge[] = [ 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' }, { id: 'e1-3', source: '1a', target: '3a' },
]; ];
@@ -83,6 +83,8 @@ const edgesB: Edge[] = [
{ id: 'e4b', source: 'inputb', target: '4b' }, { id: 'e4b', source: 'inputb', target: '4b' },
]; ];
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node.position);
const BasicFlow = () => { const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA); const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA); const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
@@ -98,7 +100,7 @@ const BasicFlow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
disableKeyboardA11y onNodeDrag={onNodeDrag}
> >
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> <div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button <button
@@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react'; import { MouseEvent, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
useReactFlow, useReactFlow,
NodeTypes, NodeTypes,
@@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els)); const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useReactFlow(); const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
+7
View File
@@ -1,5 +1,12 @@
# @reactflow/background # @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 ## 11.1.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reactflow/background", "name": "@reactflow/background",
"version": "11.1.2", "version": "11.1.3",
"description": "Background component with different variants for React Flow", "description": "Background component with different variants for React Flow",
"keywords": [ "keywords": [
"react", "react",
+7
View File
@@ -1,5 +1,12 @@
# @reactflow/controls # @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 ## 11.1.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reactflow/controls", "name": "@reactflow/controls",
"version": "11.1.2", "version": "11.1.3",
"description": "Component to control the viewport of a React Flow instance", "description": "Component to control the viewport of a React Flow instance",
"keywords": [ "keywords": [
"react", "react",
+25
View File
@@ -1,5 +1,30 @@
# @reactflow/core # @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 ## 11.4.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reactflow/core", "name": "@reactflow/core",
"version": "11.4.2", "version": "11.5.0",
"description": "Core components and util functions of React Flow.", "description": "Core components and util functions of React Flow.",
"keywords": [ "keywords": [
"react", "react",
@@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils'; 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'; import { Position, ConnectionLineType, ConnectionMode } from '../../types';
type ConnectionLineProps = { type ConnectionLineProps = {
connectionNodeId: string; nodeId: string;
connectionHandleType: HandleType; handleType: HandleType;
connectionLineType: ConnectionLineType; type: ConnectionLineType;
isConnectable: boolean; style?: CSSProperties;
connectionLineStyle?: CSSProperties; CustomComponent?: ConnectionLineComponent;
CustomConnectionLineComponent?: ConnectionLineComponent;
}; };
const oppositePosition = { const oppositePosition = {
@@ -26,69 +25,62 @@ const oppositePosition = {
}; };
const ConnectionLine = ({ const ConnectionLine = ({
connectionNodeId, nodeId,
connectionHandleType, handleType,
connectionLineStyle, style,
connectionLineType = ConnectionLineType.Bezier, type = ConnectionLineType.Bezier,
isConnectable, CustomComponent,
CustomConnectionLineComponent,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore( const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback( useCallback(
(s: ReactFlowStore) => ({ (s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(connectionNodeId), fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId, handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
}), }),
[connectionNodeId] [nodeId]
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
let handleBounds = fromHandleBounds?.[connectionHandleType]; let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) { if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
? handleBounds
: fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source'];
} }
if (!fromNode || !isConnectable || !handleBounds) { if (!fromNode || !handleBounds) {
return null; return null;
} }
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX; const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY; const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition) { if (!fromPosition || !toPosition) {
return null; return null;
} }
const toPosition: Position = oppositePosition[fromPosition]; if (CustomComponent) {
if (CustomConnectionLineComponent) {
return ( return (
<g className="react-flow__connection"> <CustomComponent
<CustomConnectionLineComponent connectionLineType={type}
connectionLineType={connectionLineType} connectionLineStyle={style}
connectionLineStyle={connectionLineStyle} fromNode={fromNode}
fromNode={fromNode} fromHandle={fromHandle}
fromHandle={fromHandle} fromX={fromX}
fromX={fromX} fromY={fromY}
fromY={fromY} toX={toX}
toX={toX} toY={toY}
toY={toY} fromPosition={fromPosition}
fromPosition={fromPosition} toPosition={toPosition}
toPosition={toPosition} />
/>
</g>
); );
} }
@@ -103,29 +95,62 @@ const ConnectionLine = ({
targetPosition: toPosition, targetPosition: toPosition,
}; };
if (connectionLineType === ConnectionLineType.Bezier) { if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position // we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams); [dAttr] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) { } else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({ [dAttr] = getSmoothStepPath({
...pathParams, ...pathParams,
borderRadius: 0, borderRadius: 0,
}); });
} else if (connectionLineType === ConnectionLineType.SmoothStep) { } else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams); [dAttr] = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) { } else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams); [dAttr] = getSimpleBezierPath(pathParams);
} else { } else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`; dAttr = `M${fromX},${fromY} ${toX},${toY}`;
} }
return ( return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
<g className="react-flow__connection">
<path d={dAttr} fill="none" className="react-flow__connection-path" style={connectionLineStyle} />
</g>
);
}; };
ConnectionLine.displayName = 'ConnectionLine'; 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 type { ReactNode } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { ReactFlowState } from '../../types'; import { ReactFlowState } from '../../types';
@@ -1,6 +1,6 @@
import EdgeText from './EdgeText'; import EdgeText from './EdgeText';
import type { BaseEdgeProps } from '../../types';
import { isNumeric } from '../../utils'; import { isNumeric } from '../../utils';
import type { BaseEdgeProps } from '../../types';
const BaseEdge = ({ const BaseEdge = ({
path, path,
@@ -4,7 +4,7 @@ import cc from 'classcat';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { handleMouseDown } from '../Handle/handler'; import { handlePointerDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph'; import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
@@ -99,14 +99,14 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
setUpdating(true); setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType); onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent) => { const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false); setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType); onEdgeUpdateEnd?.(evt, edge, handleType);
}; };
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
handleMouseDown({ handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
@@ -115,7 +115,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
getState: store.getState, getState: store.getState,
setState: store.setState, setState: store.setState,
isValidConnection, isValidConnection,
elementEdgeUpdaterType: handleType, edgeUpdaterType: handleType,
onEdgeUpdateEnd: _onEdgeUpdateEnd, 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 { StoreApi } from 'zustand';
import { getHostForElement } from '../../utils'; import { getHostForElement, calcAutoPan, getEventPosition } from '../../utils';
import { ConnectionMode } from '../../types'; import type { OnConnect, HandleType, ReactFlowState } from '../../types';
import type { OnConnect, Connection, 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; export function handlePointerDown({
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({
event, event,
handleId, handleId,
nodeId, nodeId,
@@ -84,10 +23,10 @@ export function handleMouseDown({
getState, getState,
setState, setState,
isValidConnection, isValidConnection,
elementEdgeUpdaterType, edgeUpdaterType,
onEdgeUpdateEnd, onEdgeUpdateEnd,
}: { }: {
event: ReactMouseEvent; event: ReactMouseEvent | ReactTouchEvent;
handleId: string | null; handleId: string | null;
nodeId: string; nodeId: string;
onConnect: OnConnect; onConnect: OnConnect;
@@ -95,35 +34,58 @@ export function handleMouseDown({
getState: StoreApi<ReactFlowState>['getState']; getState: StoreApi<ReactFlowState>['getState'];
setState: StoreApi<ReactFlowState>['setState']; setState: StoreApi<ReactFlowState>['setState'];
isValidConnection: ValidConnectionFunc; isValidConnection: ValidConnectionFunc;
elementEdgeUpdaterType?: HandleType; edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent) => void; onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): 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 // when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
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; return;
} }
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY); let prevActiveHandle: Element;
const elementBelowIsTarget = elementBelow?.classList.contains('target'); let connectionPosition = getEventPosition(event, containerBounds);
const elementBelowIsSource = elementBelow?.classList.contains('source'); let autoPanStarted = false;
if (!reactFlowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) { const handleLookup = getHandleLookup({
return; nodes: getNodes(),
} nodeId,
handleId,
handleType,
});
const { onConnectStart, connectionMode } = getState(); // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source'; const autoPan = (): void => {
const containerBounds = reactFlowNode.getBoundingClientRect(); if (!autoPanOnConnect) {
let recentHoveredHandle: Element; return;
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan);
};
setState({ setState({
connectionPosition: { connectionPosition,
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionNodeId: nodeId, connectionNodeId: nodeId,
connectionHandleId: handleId, connectionHandleId: handleId,
connectionHandleType: handleType, connectionHandleType: handleType,
@@ -131,68 +93,95 @@ export function handleMouseDown({
onConnectStart?.(event, { nodeId, handleId, handleType }); 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({ setState({
connectionPosition: { connectionPosition: prevClosestHandle
x: event.clientX - containerBounds.left, ? rendererPointToPoint(
y: event.clientY - containerBounds.top, {
}, x: prevClosestHandle.x,
y: prevClosestHandle.y,
},
transform
)
: connectionPosition,
}); });
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid( if (!prevClosestHandle) {
event, return resetRecentHandle(prevActiveHandle);
}
const { connection, handleDomNode, isValid } = isValidHandle(
prevClosestHandle,
connectionMode, connectionMode,
isTarget,
nodeId, nodeId,
handleId, handleId,
isTarget ? 'target' : 'source',
isValidConnection, isValidConnection,
doc doc
); );
if (!isHoveringHandle) { if (connection.source !== connection.target && handleDomNode) {
return resetRecentHandle(recentHoveredHandle); resetRecentHandle(prevActiveHandle);
} prevActiveHandle = handleDomNode;
handleDomNode.classList.add('react-flow__handle-connecting');
if (connection.source !== connection.target && elementBelow) { handleDomNode.classList.toggle('react-flow__handle-valid', isValid);
resetRecentHandle(recentHoveredHandle);
recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid);
} }
} }
function onMouseUp(event: MouseEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
const { connection, isValid } = checkElementBelowIsValid( cancelAnimationFrame(autoPanId);
event, autoPanStarted = false;
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
if (isValid) { if (prevClosestHandle) {
onConnect?.(connection); 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) { if (edgeUpdaterType) {
onEdgeUpdateEnd(event); onEdgeUpdateEnd?.(event);
} }
resetRecentHandle(recentHoveredHandle); resetRecentHandle(prevActiveHandle);
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); cancelConnection();
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
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('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); 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 cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { checkElementBelowIsValid, handleMouseDown } from './handler'; import { handlePointerDown } from './handler';
import { getHostForElement } from '../../utils'; import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph'; import { addEdge } from '../../utils/graph';
import { Position } from '../../types'; import { Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types'; import type { HandleProps, Connection, ReactFlowState } from '../../types';
import { isValidHandle } from './utils';
import { errorMessages } from '../../contants';
const alwaysValid = () => true; const alwaysValid = () => true;
@@ -32,14 +34,20 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
children, children,
className, className,
onMouseDown, onMouseDown,
onTouchStart,
...rest ...rest
}, },
ref ref
) => { ) => {
const store = useStoreApi(); 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 { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow);
const handleId = id || null; const handleId = id || null;
@@ -61,9 +69,11 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
onConnect?.(edgeParams); onConnect?.(edgeParams);
}; };
const onMouseDownHandler = (event: ReactMouseEvent<HTMLDivElement>) => { const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (event.button === 0) { const isMouseTriggered = isMouseEvent(event);
handleMouseDown({
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
@@ -74,7 +84,12 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isValidConnection, isValidConnection,
}); });
} }
onMouseDown?.(event);
if (isMouseTriggered) {
onMouseDown?.(event);
} else {
onTouchStart?.(event);
}
}; };
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
@@ -86,12 +101,16 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
} }
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid( const { connection, isValid } = isValidHandle(
event as unknown as MouseEvent, {
nodeId,
id: handleId,
type,
},
connectionMode, connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId, connectionStartHandle.nodeId,
connectionStartHandle.handleId || null, connectionStartHandle.handleId || null,
connectionStartHandle.type,
isValidConnection, isValidConnection,
doc doc
); );
@@ -110,6 +129,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handleid={handleId} data-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
data-id={`${nodeId}-${handleId}-${type}`}
className={cc([ className={cc([
'react-flow__handle', 'react-flow__handle',
`react-flow__handle-${position}`, `react-flow__handle-${position}`,
@@ -126,7 +146,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionStartHandle?.type === type, connectionStartHandle?.type === type,
}, },
])} ])}
onMouseDown={onMouseDownHandler} onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined} onClick={connectOnClick ? onClick : undefined}
ref={ref} ref={ref}
{...rest} {...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; disableKeyboardA11y: boolean;
} }
const selector = (s: ReactFlowState) => ({ 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 selectedNodes = s.getNodes().filter((n) => n.selected); 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) { function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) {
const store = useStoreApi(); const store = useStoreApi();
const { transformString, userSelectionActive } = useStore(selector, shallow); const { width, height, x: left, y: top, transformString, userSelectionActive } = useStore(selector, shallow);
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const updatePositions = useUpdateNodePositions(); const updatePositions = useUpdateNodePositions();
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
@@ -45,6 +45,10 @@ type StoreUpdaterProps = Pick<
| 'noPanClassName' | 'noPanClassName'
| 'nodeOrigin' | 'nodeOrigin'
| 'elevateNodesOnSelect' | 'elevateNodesOnSelect'
| 'autoPanOnConnect'
| 'autoPanOnNodeDrag'
| 'onError'
| 'connectionRadius'
> & { rfId: string }; > & { rfId: string };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -119,6 +123,10 @@ const StoreUpdater = ({
noPanClassName, noPanClassName,
nodeOrigin, nodeOrigin,
rfId, rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionRadius,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
@@ -172,6 +180,10 @@ const StoreUpdater = ({
useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState); useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState);
useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState); useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState);
useDirectStoreUpdater('rfId', rfId, 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<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges); useStoreUpdater<Edge[]>(edges, setEdges);
@@ -10,9 +10,9 @@ const selector = (s: ReactFlowState) => ({
function UserSelection() { function UserSelection() {
const { userSelectionActive, userSelectionRect } = useStore(selector, shallow); const { userSelectionActive, userSelectionRect } = useStore(selector, shallow);
const showSelectionBox = userSelectionActive && userSelectionRect; const isActive = userSelectionActive && userSelectionRect;
if (!showSelectionBox) { if (!isActive) {
return null; return null;
} }
@@ -1,8 +1,9 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { devWarn } from '../../utils';
import { MarkerType } from '../../types'; import { MarkerType } from '../../types';
import type { EdgeMarker } from '../../types'; import type { EdgeMarker } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
import { errorMessages } from '../../contants';
type SymbolProps = Omit<EdgeMarker, 'type'>; type SymbolProps = Omit<EdgeMarker, 'type'>;
@@ -38,17 +39,20 @@ export const MarkerSymbols = {
}; };
export function useMarkerSymbol(type: MarkerType) { export function useMarkerSymbol(type: MarkerType) {
const store = useStoreApi();
const symbol = useMemo(() => { const symbol = useMemo(() => {
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type); const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
if (!symbolExists) { 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 null;
} }
return MarkerSymbols[type]; return MarkerSymbols[type];
}, [type]); }, [type]);
return symbol; return symbol;
} }
@@ -1,27 +1,20 @@
import { memo } from 'react'; import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdges from '../../hooks/useVisibleEdges';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils'; import { getEdgePositions, getHandle, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import { devWarn } from '../../utils';
import { ConnectionMode, Position } from '../../types'; import { ConnectionMode, Position } from '../../types';
import type { Edge, ReactFlowState } from '../../types'; import type { Edge, ReactFlowState } from '../../types';
import { errorMessages } from '../../contants';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
| 'edgeTypes' | 'edgeTypes'
| 'connectionLineType'
| 'connectionLineType'
| 'connectionLineStyle'
| 'connectionLineComponent'
| 'connectionLineContainerStyle'
| 'connectionLineContainerStyle'
| 'onEdgeClick' | 'onEdgeClick'
| 'onEdgeDoubleClick' | 'onEdgeDoubleClick'
| 'defaultMarkerColor' | 'defaultMarkerColor'
@@ -40,11 +33,10 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
> & { > & {
elevateEdgesOnSelect: boolean; elevateEdgesOnSelect: boolean;
children: ReactNode;
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable, edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
@@ -52,35 +44,38 @@ const selector = (s: ReactFlowState) => ({
height: s.height, height: s.height,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals, nodeInternals: s.nodeInternals,
onError: s.onError,
}); });
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = ({
const { defaultMarkerColor,
connectionNodeId, onlyRenderVisibleElements,
connectionHandleType, elevateEdgesOnSelect,
nodesConnectable, rfId,
edgesFocusable, edgeTypes,
elementsSelectable, noPanClassName,
width, onEdgeUpdate,
height, onEdgeContextMenu,
connectionMode, onEdgeMouseEnter,
nodeInternals, onEdgeMouseMove,
} = useStore(selector, shallow); onEdgeMouseLeave,
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect); 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) { if (!width) {
return null; return null;
} }
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return ( return (
<> <>
{edgeTree.map(({ level, edges, isMaxLevel }) => ( {edgeTree.map(({ level, edges, isMaxLevel }) => (
@@ -91,11 +86,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
height={height} height={height}
className="react-flow__edges react-flow__container" className="react-flow__edges react-flow__container"
> >
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={props.rfId} />} {isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g> <g>
{edges.map((edge: Edge) => { {edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!); const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!); const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) { if (!sourceIsValid || !targetIsValid) {
return null; return null;
@@ -103,32 +98,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
let edgeType = edge.type || 'default'; let edgeType = edge.type || 'default';
if (!props.edgeTypes[edgeType]) { if (!edgeTypes[edgeType]) {
devWarn( onError?.('011', errorMessages['011'](edgeType));
`Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
edgeType = 'default'; edgeType = 'default';
} }
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
// when connection type is loose we can define all handles as sources // when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles = const targetNodeHandles =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target ? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null); const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null); const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) { if (!sourceHandle || !targetHandle) {
devWarn( onError?.('008', errorMessages['008'](sourceHandle, edge));
`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`
);
return null; return null;
} }
@@ -146,7 +134,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<EdgeComponent <EdgeComponent
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
className={cc([edge.className, props.noPanClassName])} className={cc([edge.className, noPanClassName])}
type={edgeType} type={edgeType}
data={edge.data} data={edge.data}
selected={!!edge.selected} selected={!!edge.selected}
@@ -172,17 +160,17 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
onEdgeUpdate={props.onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onContextMenu={props.onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove} onMouseMove={onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave} onMouseLeave={onEdgeMouseLeave}
onClick={props.onEdgeClick} onClick={onEdgeClick}
edgeUpdaterRadius={props.edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={props.rfId} rfId={rfId}
ariaLabel={edge.ariaLabel} ariaLabel={edge.ariaLabel}
isFocusable={isFocusable} isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
@@ -193,23 +181,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
</g> </g>
</svg> </svg>
))} ))}
{renderConnectionLine && ( {children}
<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>
)}
</> </>
); );
}; };
@@ -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) { if (!bounds) {
return null; return null;
} }
// there is no handleId when there are no multiple handles/ handles with ids if (handleId) {
// so we just pick the first one return bounds.find((d) => d.id === handleId)!;
let handle: HandleElement | null = null; } else if (bounds.length === 1) {
if (bounds.length === 1 || !handleId) { return bounds[0];
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId)!;
} }
return typeof handle === 'undefined' ? null : handle; return null;
} }
interface EdgePositions { interface EdgePositions {
@@ -167,16 +164,15 @@ export function isEdgeVisible({
return overlappingArea > 0; 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 handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isInvalid = const isValid =
!node || handleBounds &&
!handleBounds || node?.width &&
!node.width || node?.height &&
!node.height || typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node.positionAbsolute?.x === 'undefined' || typeof node?.positionAbsolute?.y !== 'undefined';
typeof node.positionAbsolute?.y === 'undefined';
return [ return [
{ {
@@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean
height: node?.height || 0, height: node?.height || 0,
}, },
handleBounds, handleBounds,
!isInvalid, !!isValid,
]; ];
} }
@@ -5,6 +5,7 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer'; import EdgeRenderer from '../EdgeRenderer';
import ViewportWrapper from '../Viewport'; import ViewportWrapper from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler'; import useOnInitHandler from '../../hooks/useOnInitHandler';
import ConnectionLine from '../../components/ConnectionLine';
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types'; import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
export type GraphViewProps = Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> & export type GraphViewProps = Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> &
@@ -149,10 +150,6 @@ const GraphView = ({
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
@@ -167,7 +164,14 @@ const GraphView = ({
elevateEdgesOnSelect={!!elevateEdgesOnSelect} elevateEdgesOnSelect={!!elevateEdgesOnSelect}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId} rfId={rfId}
/> >
<ConnectionLine
style={connectionLineStyle}
type={connectionLineType}
component={connectionLineComponent}
containerStyle={connectionLineContainerStyle}
/>
</EdgeRenderer>
<div className="react-flow__edgelabel-renderer" /> <div className="react-flow__edgelabel-renderer" />
<NodeRenderer <NodeRenderer
@@ -4,12 +4,13 @@ import { shallow } from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { clampPosition, devWarn, internalsSymbol } from '../../utils'; import { clampPosition, internalsSymbol } from '../../utils';
import { containerStyle } from '../../styles'; import { containerStyle } from '../../styles';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import { getPositionWithOrigin } from './utils'; import { getPositionWithOrigin } from './utils';
import { Position } from '../../types'; import { Position } from '../../types';
import type { ReactFlowState, WrapNodeProps } from '../../types'; import type { ReactFlowState, WrapNodeProps } from '../../types';
import { errorMessages } from '../../contants';
type NodeRendererProps = Pick< type NodeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -36,13 +37,12 @@ const selector = (s: ReactFlowState) => ({
nodesFocusable: s.nodesFocusable, nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions, updateNodeDimensions: s.updateNodeDimensions,
onError: s.onError,
}); });
const NodeRenderer = (props: NodeRendererProps) => { const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore( const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
selector, useStore(selector, shallow);
shallow
);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>(); const resizeObserverRef = useRef<ResizeObserver>();
@@ -78,9 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
let nodeType = node.type || 'default'; let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) { if (!props.nodeTypes[nodeType]) {
devWarn( onError?.('003', errorMessages['003'](nodeType));
`Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
nodeType = 'default'; nodeType = 'default';
} }
@@ -5,7 +5,6 @@ import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode'; import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode'; import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode'; import wrapNode from '../../components/Nodes/wrapNode';
import { devWarn } from '../../utils';
import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types'; import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped; 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) { if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
devWarn('nodeOrigin must be between 0 and 1');
return { x, y }; return { x, y };
} }
+4 -10
View File
@@ -11,8 +11,9 @@ import { containerStyle } from '../../styles';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges } from '../../utils/changes'; import { getSelectionChanges } from '../../utils/changes';
import { getConnectedEdges, getNodesInside } from '../../utils/graph'; import { getConnectedEdges, getNodesInside } from '../../utils/graph';
import { getEventPosition } from '../../utils';
import { SelectionMode } from '../../types'; import { SelectionMode } from '../../types';
import type { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = { type PaneProps = {
isSelecting: boolean; 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 = ( const wrapHandler = (
handler: React.MouseEventHandler | undefined, handler: React.MouseEventHandler | undefined,
containerRef: React.MutableRefObject<HTMLDivElement | null> containerRef: React.MutableRefObject<HTMLDivElement | null>
@@ -118,7 +112,7 @@ const Pane = memo(
return; return;
} }
const { x, y } = getMousePosition(event, containerBounds.current); const { x, y } = getEventPosition(event, containerBounds.current);
resetSelectedElements(); resetSelectedElements();
@@ -145,7 +139,7 @@ const Pane = memo(
store.setState({ userSelectionActive: true, nodesSelectionActive: false }); store.setState({ userSelectionActive: true, nodesSelectionActive: false });
const mousePos = getMousePosition(event, containerBounds.current); const mousePos = getEventPosition(event, containerBounds.current);
const startX = userSelectionRect.startX ?? 0; const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0; const startY = userSelectionRect.startY ?? 0;
@@ -160,6 +160,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
elevateNodesOnSelect = true, elevateNodesOnSelect = true,
elevateEdgesOnSelect = false, elevateEdgesOnSelect = false,
disableKeyboardA11y = false, disableKeyboardA11y = false,
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
connectionRadius = 20,
onError,
style, style,
id, id,
...rest ...rest
@@ -287,6 +291,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
rfId={rfId} rfId={rfId}
autoPanOnConnect={autoPanOnConnect}
autoPanOnNodeDrag={autoPanOnNodeDrag}
onError={onError}
connectionRadius={connectionRadius}
/> />
<SelectionListener onSelectionChange={onSelectionChange} /> <SelectionListener onSelectionChange={onSelectionChange} />
{children} {children}
@@ -5,6 +5,7 @@ import { devWarn } from '../../utils';
import { CreateEdgeTypes } from '../EdgeRenderer/utils'; import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils'; import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types'; import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { errorMessages } from '../../contants';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped; 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') { if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes); const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) { if (shallow(typesKeysRef.current, typeKeys)) {
devWarn( devWarn('002', errorMessages['002']());
"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"
);
} }
typesKeysRef.current = typeKeys; 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 { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils'; import { handleNodeClick } from '../../components/Nodes/utils';
import useGetPointerPosition from '../useGetPointerPosition'; 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 }; export type UseDragData = { dx: number; dy: number };
@@ -34,10 +35,15 @@ function useDrag({
isSelectable, isSelectable,
selectNodesOnDrag, selectNodesOnDrag,
}: UseDragParams) { }: UseDragParams) {
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi(); 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 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(); const getPointerPosition = useGetPointerPosition();
@@ -45,6 +51,80 @@ function useDrag({
if (nodeRef?.current) { if (nodeRef?.current) {
const selection = select(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) { if (disabled) {
selection.on('.drag', null); selection.on('.drag', null);
} else { } else {
@@ -56,6 +136,7 @@ function useDrag({
unselectNodesAndEdges, unselectNodesAndEdges,
onNodeDragStart, onNodeDragStart,
onSelectionDragStart, onSelectionDragStart,
domNode,
} = store.getState(); } = store.getState();
const onStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart); const onStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
@@ -86,72 +167,35 @@ function useDrag({
}); });
onStart(event.sourceEvent as MouseEvent, currentNode, nodes); onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
} }
containerBounds.current = domNode?.getBoundingClientRect() || null;
mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
}) })
.on('drag', (event: UseDragEvent) => { .on('drag', (event: UseDragEvent) => {
const {
updateNodePositions,
nodeInternals,
nodeExtent,
onNodeDrag,
onSelectionDrag,
snapGrid,
snapToGrid,
nodeOrigin,
} = store.getState();
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
const { autoPanOnNodeDrag } = store.getState();
if (!autoPanStarted.current && autoPanOnNodeDrag) {
autoPanStarted.current = true;
autoPan();
}
// skip events without movement // skip events without movement
if ( if (
(lastPos.current.x !== pointerPos.xSnapped || lastPos.current.y !== pointerPos.ySnapped) && (lastPos.current.x !== pointerPos.xSnapped || lastPos.current.y !== pointerPos.ySnapped) &&
dragItems.current dragItems.current
) { ) {
lastPos.current = { dragEvent.current = event.sourceEvent as MouseEvent;
x: pointerPos.xSnapped, mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
y: pointerPos.ySnapped,
};
let hasChange = false; updateNodes(pointerPos);
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);
}
} }
}) })
.on('end', (event: UseDragEvent) => { .on('end', (event: UseDragEvent) => {
setDragging(false); setDragging(false);
autoPanStarted.current = false;
cancelAnimationFrame(autoPanId.current);
if (dragItems.current) { if (dragItems.current) {
const { updateNodePositions, nodeInternals, onNodeDragStop, onSelectionDragStop } = store.getState(); const { updateNodePositions, nodeInternals, onNodeDragStop, onSelectionDragStop } = store.getState();
const onStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop); const onStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
+6 -4
View File
@@ -1,8 +1,9 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { clampPosition, devWarn, isNumeric } from '../../utils'; import { clampPosition, isNumeric } from '../../utils';
import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, OnError, XYPosition } from '../../types';
import { getNodePositionWithOrigin } from '../../utils/graph'; import { getNodePositionWithOrigin } from '../../utils/graph';
import { errorMessages } from '../../contants';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) { if (!node.parentNode) {
@@ -62,7 +63,8 @@ export function calcNextPosition(
nextPosition: XYPosition, nextPosition: XYPosition,
nodeInternals: NodeInternals, nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent, nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0],
onError?: OnError
): { position: XYPosition; positionAbsolute: XYPosition } { ): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent; let currentExtent = node.extent || nodeExtent;
@@ -81,7 +83,7 @@ export function calcNextPosition(
] ]
: currentExtent; : currentExtent;
} else { } else {
devWarn('Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500'); onError?.('005', errorMessages['005']());
currentExtent = nodeExtent; currentExtent = nodeExtent;
} }
+3 -4
View File
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { useStoreApi } from '../hooks/useStore'; 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 { function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi(); const store = useStoreApi();
@@ -18,9 +19,7 @@ function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>)
const size = getDimensions(rendererNode.current); const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) { if (size.height === 0 || size.width === 0) {
devWarn( store.getState().onError?.('004', errorMessages['004']());
'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
);
} }
store.setState({ width: size.width || 500, height: size.height || 500 }); 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 type { StoreApi } from 'zustand';
import StoreContext from '../contexts/RFStoreContext'; import StoreContext from '../contexts/RFStoreContext';
import { errorMessages } from '../contants';
import type { ReactFlowState } from '../types'; import type { ReactFlowState } from '../types';
const errorMessage = const zustandErrorMessage = errorMessages['001']();
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#100';
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never; type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
@@ -17,7 +17,7 @@ function useStore<StateSlice = ExtractState>(
const store = useContext(StoreContext); const store = useContext(StoreContext);
if (store === null) { if (store === null) {
throw new Error(errorMessage); throw new Error(zustandErrorMessage);
} }
return useZustandStore(store, selector, equalityFn); return useZustandStore(store, selector, equalityFn);
@@ -27,7 +27,7 @@ const useStoreApi = () => {
const store = useContext(StoreContext); const store = useContext(StoreContext);
if (store === null) { if (store === null) {
throw new Error(errorMessage); throw new Error(zustandErrorMessage);
} }
return useMemo( return useMemo(
@@ -7,7 +7,8 @@ function useUpdateNodePositions() {
const store = useStoreApi(); const store = useStoreApi();
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { 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); const selectedNodes = getNodes().filter((n) => n.selected);
// by default a node moves 5px on each key press, or 20px if shift is pressed // 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. // 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]); 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.position = position;
n.positionAbsolute = updatedPos.positionAbsolute; n.positionAbsolute = positionAbsolute;
} }
return n; return n;
+20
View File
@@ -1,4 +1,5 @@
import { createStore } from 'zustand'; import { createStore } from 'zustand';
import { zoomIdentity } from 'd3-zoom';
import { clampPosition, getDimensions, internalsSymbol } from '../utils'; import { clampPosition, getDimensions, internalsSymbol } from '../utils';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -18,6 +19,7 @@ import type {
NodeDragItem, NodeDragItem,
UnselectNodesAndEdgesParams, UnselectNodesAndEdgesParams,
NodeChange, NodeChange,
XYPosition,
} from '../types'; } from '../types';
const createRFStore = () => const createRFStore = () =>
@@ -250,10 +252,28 @@ const createRFStore = () =>
nodeInternals: new Map(nodeInternals), 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: () => cancelConnection: () =>
set({ set({
connectionNodeId: initialState.connectionNodeId, connectionNodeId: initialState.connectionNodeId,
connectionHandleId: initialState.connectionHandleId, connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
}), }),
reset: () => set({ ...initialState }), reset: () => set({ ...initialState }),
})); }));
+5
View File
@@ -1,3 +1,4 @@
import { devWarn } from '../utils';
import { ConnectionMode } from '../types'; import { ConnectionMode } from '../types';
import type { CoordinateExtent, ReactFlowStore } from '../types'; import type { CoordinateExtent, ReactFlowStore } from '../types';
@@ -56,6 +57,10 @@ const initialState: ReactFlowStore = {
connectOnClick: true, connectOnClick: true,
ariaLiveMessage: '', ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
connectionRadius: 20,
onError: devWarn,
}; };
export default initialState; export default initialState;
+1 -1
View File
@@ -98,7 +98,7 @@
.react-flow__connection { .react-flow__connection {
pointer-events: none; pointer-events: none;
&.animated { .animated {
stroke-dasharray: 5; stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite; animation: dashdraw 0.5s linear infinite;
} }
+6 -1
View File
@@ -36,6 +36,7 @@ import type {
EdgeMouseHandler, EdgeMouseHandler,
HandleType, HandleType,
SelectionMode, SelectionMode,
OnError,
} from '.'; } from '.';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & { export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
@@ -61,7 +62,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
onEdgeMouseLeave?: EdgeMouseHandler; onEdgeMouseLeave?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler; onEdgeDoubleClick?: EdgeMouseHandler;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; 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; onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange; onEdgesChange?: OnEdgesChange;
onNodesDelete?: OnNodesDelete; onNodesDelete?: OnNodesDelete;
@@ -139,6 +140,10 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
elevateNodesOnSelect?: boolean; elevateNodesOnSelect?: boolean;
elevateEdgesOnSelect?: boolean; elevateEdgesOnSelect?: boolean;
disableKeyboardA11y?: boolean; disableKeyboardA11y?: boolean;
autoPanOnNodeDrag?: boolean;
autoPanOnConnect?: boolean;
connectionRadius?: number;
onError?: OnError;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+1 -1
View File
@@ -85,7 +85,7 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onMouseLeave?: EdgeMouseHandler; onMouseLeave?: EdgeMouseHandler;
edgeUpdaterRadius?: number; edgeUpdaterRadius?: number;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; 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; rfId?: string;
isFocusable: boolean; isFocusable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions; pathOptions?: BezierPathOptions | SmoothStepPathOptions;
+15 -3
View File
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
@@ -77,8 +82,8 @@ export type OnConnectStartParams = {
handleType: HandleType | null; handleType: HandleType | null;
}; };
export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void; export type OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent) => void; export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type Viewport = { export type Viewport = {
x: number; x: number;
@@ -201,6 +206,7 @@ export type ReactFlowStore = {
onNodesDelete?: OnNodesDelete; onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete; onEdgesDelete?: OnEdgesDelete;
onError?: OnError;
// event handlers // event handlers
onViewportChangeStart?: OnViewportChange; onViewportChangeStart?: OnViewportChange;
@@ -210,6 +216,9 @@ export type ReactFlowStore = {
onSelectionChange?: OnSelectionChangeFunc; onSelectionChange?: OnSelectionChangeFunc;
ariaLiveMessage: string; ariaLiveMessage: string;
autoPanOnConnect: boolean;
autoPanOnNodeDrag: boolean;
connectionRadius: number;
}; };
export type ReactFlowActions = { export type ReactFlowActions = {
@@ -230,6 +239,7 @@ export type ReactFlowActions = {
cancelConnection: () => void; cancelConnection: () => void;
reset: () => void; reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void; triggerNodeChanges: (changes: NodeChange[]) => void;
panBy: (delta: XYPosition) => void;
}; };
export type ReactFlowState = ReactFlowStore & ReactFlowActions; export type ReactFlowState = ReactFlowStore & ReactFlowActions;
@@ -261,3 +271,5 @@ export type SelectionRect = Rect & {
startX: number; startX: number;
startY: 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 '.'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
// interface for the user node items // interface for the user node items
export type Node<T = any> = { export type Node<T = any, U extends string | undefined = string | undefined> = {
id: string; id: string;
position: XYPosition; position: XYPosition;
data: T; data: T;
type?: string; type?: U;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourcePosition?: Position; sourcePosition?: Position;
+11 -3
View File
@@ -13,6 +13,7 @@ import {
NodeInternals, NodeInternals,
NodeOrigin, NodeOrigin,
} from '../types'; } from '../types';
import { errorMessages } from '../contants';
export const isEdge = (element: Node | Connection | Edge): element is Edge => export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element; '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[] => { export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) { 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; 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[] => { export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) { 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; 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; const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) { 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; return edges;
} }
@@ -141,6 +142,13 @@ export const pointToRendererPoint = (
return position; 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 = ( export const getNodePositionWithOrigin = (
node: Node | undefined, node: Node | undefined,
nodeOrigin: NodeOrigin = [0, 0] 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'; import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({ 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]), 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 => export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
(element.getRootNode?.() as Document | ShadowRoot) || window?.document; (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 // used for a11y key board controls for nodes and edges
export const elementSelectionKeys = ['Enter', ' ', 'Escape']; export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
export const devWarn = (message: string) => { export const devWarn = (id: string, message: string) => {
if (process.env.NODE_ENV === 'development') { 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') !!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 # @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 ## 11.3.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reactflow/minimap", "name": "@reactflow/minimap",
"version": "11.3.2", "version": "11.3.3",
"description": "Minimap component for React Flow.", "description": "Minimap component for React Flow.",
"keywords": [ "keywords": [
"react", "react",
+11
View File
@@ -1,5 +1,16 @@
# @reactflow/node-resizer # @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 ## 1.2.2
### Patch Changes ### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reactflow/node-resizer", "name": "@reactflow/node-resizer",
"version": "1.2.2", "version": "2.0.0",
"description": "A helper component for resizing nodes.", "description": "A helper component for resizing nodes.",
"keywords": [ "keywords": [
"react", "react",
@@ -38,7 +38,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@reactflow/core": "workspace:*", "@reactflow/core": "workspace:^11.3.3",
"classcat": "^5.0.4", "classcat": "^5.0.4",
"d3-drag": "^3.0.0", "d3-drag": "^3.0.0",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
@@ -14,6 +14,7 @@ export default function NodeResizer({
color, color,
minWidth = 10, minWidth = 10,
minHeight = 10, minHeight = 10,
shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
onResizeEnd, onResizeEnd,
@@ -36,6 +37,7 @@ export default function NodeResizer({
minWidth={minWidth} minWidth={minWidth}
minHeight={minHeight} minHeight={minHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
/> />
@@ -51,6 +53,7 @@ export default function NodeResizer({
minWidth={minWidth} minWidth={minWidth}
minHeight={minHeight} minHeight={minHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
/> />
+24 -1
View File
@@ -12,6 +12,7 @@ import {
} from '@reactflow/core'; } from '@reactflow/core';
import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types';
import { getDirection } from './utils';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
@@ -31,6 +32,7 @@ function ResizeControl({
color, color,
minWidth = 10, minWidth = 10,
minHeight = 10, minHeight = 10,
shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
onResizeEnd, onResizeEnd,
@@ -140,7 +142,28 @@ function ResizeControl({
prevValues.current.height = height; 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); triggerNodeChanges(changes);
} }
}) })
+17 -5
View File
@@ -1,13 +1,24 @@
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import type { D3DragEvent, SubjectPosition } from 'd3-drag'; import type { D3DragEvent, SubjectPosition } from 'd3-drag';
export type ResizeEventParams = { export type ResizeParams = {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: 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 = { export type NodeResizerProps = {
nodeId?: string; nodeId?: string;
color?: string; color?: string;
@@ -18,9 +29,10 @@ export type NodeResizerProps = {
isVisible?: boolean; isVisible?: boolean;
minWidth?: number; minWidth?: number;
minHeight?: number; minHeight?: number;
onResizeStart?: (event: ResizeDragEvent, params: ResizeEventParams) => void; shouldResize?: ShouldResize;
onResize?: (event: ResizeDragEvent, params: ResizeEventParams) => void; onResizeStart?: OnResizeStart;
onResizeEnd?: (event: ResizeDragEvent, params: ResizeEventParams) => void; onResize?: OnResize;
onResizeEnd?: OnResizeEnd;
}; };
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
@@ -34,7 +46,7 @@ export enum ResizeControlVariant {
export type ResizeControlProps = Pick< export type ResizeControlProps = Pick<
NodeResizerProps, NodeResizerProps,
'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'onResizeStart' | 'onResize' | 'onResizeEnd' 'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'shouldResize' | 'onResizeStart' | 'onResize' | 'onResizeEnd'
> & { > & {
position?: ControlPosition; position?: ControlPosition;
variant?: ResizeControlVariant; 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" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@reactflow/core": "workspace:*", "@reactflow/core": "workspace:^11.3.3",
"classcat": "^5.0.3", "classcat": "^5.0.3",
"zustand": "^4.3.1" "zustand": "^4.3.1"
}, },
+30
View File
@@ -1,5 +1,35 @@
# reactflow # 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 ## 11.4.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "reactflow", "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", "description": "A highly customizable React library for building node-based editors and interactive flow charts",
"keywords": [ "keywords": [
"react", "react",
+56 -56
View File
@@ -32,15 +32,15 @@ importers:
'@changesets/changelog-github': registry.npmjs.org/@changesets/changelog-github/0.4.7 '@changesets/changelog-github': registry.npmjs.org/@changesets/changelog-github/0.4.7
'@changesets/cli': registry.npmjs.org/@changesets/cli/2.25.0 '@changesets/cli': registry.npmjs.org/@changesets/cli/2.25.0
'@preconstruct/cli': registry.npmjs.org/@preconstruct/cli/2.2.1 '@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/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0_equgvmdpavr6nminvx2krii6h4
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.48.1_id2eilsndvzhjjktb64trvy3gu '@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 autoprefixer: registry.npmjs.org/autoprefixer/10.4.9_postcss@8.4.21
concurrently: registry.npmjs.org/concurrently/7.6.0 concurrently: registry.npmjs.org/concurrently/7.6.0
cypress: registry.npmjs.org/cypress/10.7.0 cypress: registry.npmjs.org/cypress/10.7.0
eslint: registry.npmjs.org/eslint/8.23.1 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-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-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: registry.npmjs.org/postcss/8.4.21
postcss-cli: registry.npmjs.org/postcss-cli/10.1.0_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 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: packages/node-resizer:
specifiers: specifiers:
'@reactflow/core': workspace:* '@reactflow/core': workspace:^11.3.3
'@reactflow/eslint-config': workspace:* '@reactflow/eslint-config': workspace:*
'@reactflow/rollup-config': workspace:* '@reactflow/rollup-config': workspace:*
'@reactflow/tsconfig': workspace:* '@reactflow/tsconfig': workspace:*
@@ -248,7 +248,7 @@ importers:
packages/node-toolbar: packages/node-toolbar:
specifiers: specifiers:
'@reactflow/core': workspace:* '@reactflow/core': workspace:^11.3.3
'@reactflow/eslint-config': workspace:* '@reactflow/eslint-config': workspace:*
'@reactflow/rollup-config': workspace:* '@reactflow/rollup-config': workspace:*
'@reactflow/tsconfig': workspace:* '@reactflow/tsconfig': workspace:*
@@ -312,7 +312,7 @@ importers:
eslint: registry.npmjs.org/eslint/8.23.1 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-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-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: tooling/rollup-config:
specifiers: specifiers:
@@ -2186,11 +2186,11 @@ packages:
dev: true dev: true
optional: true optional: true
registry.npmjs.org/@typescript-eslint/eslint-plugin/5.48.1_fsnwvyoekxjd6kngphv4egujga: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0_equgvmdpavr6nminvx2krii6h4:
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} 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.48.1 id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.49.0
name: '@typescript-eslint/eslint-plugin' name: '@typescript-eslint/eslint-plugin'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^5.0.0 '@typescript-eslint/parser': ^5.0.0
@@ -2200,10 +2200,10 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/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.48.1 '@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.48.1_id2eilsndvzhjjktb64trvy3gu '@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.48.1_id2eilsndvzhjjktb64trvy3gu '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu
debug: registry.npmjs.org/debug/4.3.4 debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1 eslint: registry.npmjs.org/eslint/8.23.1
ignore: registry.npmjs.org/ignore/5.2.4 ignore: registry.npmjs.org/ignore/5.2.4
@@ -2216,11 +2216,11 @@ packages:
- supports-color - supports-color
dev: true dev: true
registry.npmjs.org/@typescript-eslint/parser/5.48.1_id2eilsndvzhjjktb64trvy3gu: registry.npmjs.org/@typescript-eslint/parser/5.49.0_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} 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.48.1 id: registry.npmjs.org/@typescript-eslint/parser/5.49.0
name: '@typescript-eslint/parser' name: '@typescript-eslint/parser'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -2229,9 +2229,9 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1 '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1 '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4 '@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 debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1 eslint: registry.npmjs.org/eslint/8.23.1
typescript: registry.npmjs.org/typescript/4.9.4 typescript: registry.npmjs.org/typescript/4.9.4
@@ -2239,21 +2239,21 @@ packages:
- supports-color - supports-color
dev: true dev: true
registry.npmjs.org/@typescript-eslint/scope-manager/5.48.1: registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0:
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} 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' name: '@typescript-eslint/scope-manager'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: 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
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1 '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0
dev: true dev: true
registry.npmjs.org/@typescript-eslint/type-utils/5.48.1_id2eilsndvzhjjktb64trvy3gu: registry.npmjs.org/@typescript-eslint/type-utils/5.49.0_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} 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.48.1 id: registry.npmjs.org/@typescript-eslint/type-utils/5.49.0
name: '@typescript-eslint/type-utils' name: '@typescript-eslint/type-utils'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: '*' eslint: '*'
@@ -2262,8 +2262,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4 '@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.48.1_id2eilsndvzhjjktb64trvy3gu '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu
debug: registry.npmjs.org/debug/4.3.4 debug: registry.npmjs.org/debug/4.3.4
eslint: registry.npmjs.org/eslint/8.23.1 eslint: registry.npmjs.org/eslint/8.23.1
tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.9.4 tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.9.4
@@ -2272,18 +2272,18 @@ packages:
- supports-color - supports-color
dev: true dev: true
registry.npmjs.org/@typescript-eslint/types/5.48.1: registry.npmjs.org/@typescript-eslint/types/5.49.0:
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} 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' name: '@typescript-eslint/types'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4: registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0_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} 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.48.1 id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.49.0
name: '@typescript-eslint/typescript-estree' name: '@typescript-eslint/typescript-estree'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
@@ -2291,8 +2291,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: 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
'@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1 '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0
debug: registry.npmjs.org/debug/4.3.4 debug: registry.npmjs.org/debug/4.3.4
globby: registry.npmjs.org/globby/11.1.0 globby: registry.npmjs.org/globby/11.1.0
is-glob: registry.npmjs.org/is-glob/4.0.3 is-glob: registry.npmjs.org/is-glob/4.0.3
@@ -2303,20 +2303,20 @@ packages:
- supports-color - supports-color
dev: true dev: true
registry.npmjs.org/@typescript-eslint/utils/5.48.1_id2eilsndvzhjjktb64trvy3gu: registry.npmjs.org/@typescript-eslint/utils/5.49.0_id2eilsndvzhjjktb64trvy3gu:
resolution: {integrity: sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.1.tgz} 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.48.1 id: registry.npmjs.org/@typescript-eslint/utils/5.49.0
name: '@typescript-eslint/utils' name: '@typescript-eslint/utils'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies: dependencies:
'@types/json-schema': registry.npmjs.org/@types/json-schema/7.0.11 '@types/json-schema': registry.npmjs.org/@types/json-schema/7.0.11
'@types/semver': registry.npmjs.org/@types/semver/7.3.13 '@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/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.49.0
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.48.1 '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.49.0
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4 '@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: registry.npmjs.org/eslint/8.23.1
eslint-scope: registry.npmjs.org/eslint-scope/5.1.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 eslint-utils: registry.npmjs.org/eslint-utils/3.0.0_eslint@8.23.1
@@ -2326,13 +2326,13 @@ packages:
- typescript - typescript
dev: true dev: true
registry.npmjs.org/@typescript-eslint/visitor-keys/5.48.1: registry.npmjs.org/@typescript-eslint/visitor-keys/5.49.0:
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} 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' name: '@typescript-eslint/visitor-keys'
version: 5.48.1 version: 5.49.0
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: 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 eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys/3.3.0
dev: true dev: true
@@ -3723,11 +3723,11 @@ packages:
prettier-linter-helpers: registry.npmjs.org/prettier-linter-helpers/1.0.0 prettier-linter-helpers: registry.npmjs.org/prettier-linter-helpers/1.0.0
dev: true dev: true
registry.npmjs.org/eslint-plugin-react/7.32.0_eslint@8.23.1: registry.npmjs.org/eslint-plugin-react/7.32.1_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} 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.0 id: registry.npmjs.org/eslint-plugin-react/7.32.1
name: eslint-plugin-react name: eslint-plugin-react
version: 7.32.0 version: 7.32.1
engines: {node: '>=4'} engines: {node: '>=4'}
peerDependencies: peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8