Merge pull request #2152 from wbkd/next-release/v10.3

Next release/v10.3
This commit is contained in:
Moritz Klack
2022-05-23 16:23:22 +02:00
committed by GitHub
50 changed files with 7130 additions and 9022 deletions
+1 -1
View File
@@ -106,7 +106,7 @@ You can also use our [contact form](https://pro.reactflow.dev/contact) or join t
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in your browser. Under the hood, React Flow depends on these great libraries:
* [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
* [react-draggable](https://github.com/react-grid-layout/react-draggable) - used for making the nodes draggable
* [d3-drag](https://github.com/d3/d3-drag) - used for making the nodes draggable
* [zustand](https://github.com/pmndrs/zustand) - internal state management
## License
+4 -2
View File
@@ -56,7 +56,9 @@ describe('Basic Flow Rendering', () => {
.wait(50)
.trigger('mouseup', 1, 200, { force: true });
cy.get('.react-flow__node').eq(3).should('have.class', 'selected');
cy.wait(100);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.not.class', 'selected');
@@ -120,7 +122,7 @@ describe('Basic Flow Rendering', () => {
.wait(50)
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__edge').should('have.length', 3);
});
// @TODO: why does this fail since react18?
+8 -7
View File
@@ -37,14 +37,15 @@ describe('Minimap Testing', () => {
const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
cy.wait(1000);
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
cy.drag('.react-flow__node:first', { x: 500, y: 25 })
.wait(100)
.then(() => {
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
});
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
});
});
it('changes node positions via pane drag', () => {
+9 -5
View File
@@ -26,9 +26,13 @@
Cypress.Commands.add('drag', (selector, { x, y }) => {
return cy
.get(selector)
.trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: x, clientY: y })
.wait(50)
.trigger('mouseup', { force: true });
.window()
.then((window) =>
cy
.get(selector)
.trigger('mousedown', { which: 1, view: window })
.trigger('mousemove', { clientX: x, clientY: y, force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true })
);
});
+4116 -6710
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -37,7 +37,7 @@
"@types/dagre": "^0.7.47",
"@types/localforage": "0.0.34",
"@types/react": "file:../node_modules/@types/react",
"@types/react-dom": "^18.0.3",
"@types/react-dom": "file:../node_modules/@types/react-dom",
"@types/react-router-dom": "^5.3.3"
}
}
+1
View File
@@ -66,6 +66,7 @@ const BasicFlow = () => {
maxZoom={4}
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
>
<Background variant={BackgroundVariant.Lines} />
+12 -1
View File
@@ -1,3 +1,4 @@
import { MouseEvent } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import DragHandleNode from './DragHandleNode';
@@ -17,13 +18,23 @@ const initialNodes: Node[] = [
},
];
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialEdges: Edge[] = [];
const DragHandleFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges] = useEdgesState(initialEdges);
return <ReactFlow nodes={nodes} onNodesChange={onNodesChange} edges={edges} nodeTypes={nodeTypes} />;
return (
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
/>
);
};
export default DragHandleFlow;
+10 -1
View File
@@ -9,7 +9,6 @@ import ReactFlow, {
MarkerType,
MiniMap,
Node,
Position,
ReactFlowInstance,
useEdgesState,
useNodesState,
@@ -20,6 +19,11 @@ import CustomEdge2 from './CustomEdge2';
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('dblclick', edge);
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('enter', edge);
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('move', edge);
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('leave', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
@@ -131,6 +135,11 @@ const EdgesFlow = () => {
onInit={onInit}
snapToGrid={true}
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<MiniMap />
<Controls />
+5 -5
View File
@@ -1,4 +1,4 @@
import { MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
import { MouseEvent as ReactMouseEvent, CSSProperties, useCallback } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
@@ -15,9 +15,9 @@ import ReactFlow, {
OnSelectionChangeParams,
} from 'react-flow-renderer';
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
@@ -161,7 +161,7 @@ const nodeColor = (n: Node): string => {
const OverviewFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
+1 -1
View File
@@ -23,7 +23,7 @@ const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log(reactFlowInstance.getNodes());
};
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(5, 5);
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
const StressFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
+4 -7
View File
@@ -15,7 +15,7 @@ import ReactFlow, {
} from 'react-flow-renderer';
import DebugNode from './DebugNode';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
@@ -105,15 +105,12 @@ const nodeTypes = {
default: DebugNode,
};
const BasicFlow = () => {
const Subflow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
@@ -194,4 +191,4 @@ const BasicFlow = () => {
);
};
export default BasicFlow;
export default Subflow;
+9 -1
View File
@@ -33,7 +33,11 @@ const initialEdges: Edge[] = [];
const TouchDeviceFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), []);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
const onConnectStart = useCallback(() => console.log('connect start'), []);
const onConnectStop = useCallback(() => console.log('connect end'), []);
const onClickConnectStart = useCallback(() => console.log('click connect start'), []);
const onClickConnectStop = useCallback(() => console.log('click connect end'), []);
return (
<ReactFlow
@@ -42,6 +46,10 @@ const TouchDeviceFlow = () => {
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onClickConnectStart={onClickConnectStart}
onClickConnectStop={onClickConnectStop}
className="touchdevice-flow"
/>
);
+2 -2
View File
@@ -86,8 +86,8 @@ const UpdatableEdge = () => {
snapToGrid={true}
onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
// onEdgeUpdateStart={onEdgeUpdateStart}
// onEdgeUpdateEnd={onEdgeUpdateEnd}
>
<Controls />
</ReactFlow>
+1982 -1335
View File
File diff suppressed because it is too large Load Diff
+22 -20
View File
@@ -1,6 +1,6 @@
{
"name": "react-flow-renderer",
"version": "10.2.3",
"version": "10.2.4-next.3",
"engines": {
"node": ">=14"
},
@@ -21,6 +21,7 @@
"./dist/style.css": "./dist/style.css",
"./dist/theme-default.css": "./dist/theme-default.css"
},
"sideEffects": false,
"types": "dist/esm/index.d.ts",
"license": "MIT",
"repository": {
@@ -36,7 +37,7 @@
"start:testserver": "npm run build:test && npm run start:examples",
"build:dev": "npm install && npm run build && cd example && npm install && npm run build",
"dev:wait": "start-server-and-test start:testserver http-get://localhost:3000",
"test": "BROWSER=none npm run dev:wait test:chrome",
"test": "cross-env BROWSER=none npm run dev:wait test:chrome",
"test:chrome": "cypress run --browser chrome --headless",
"test:firefox": "cypress run --browser firefox",
"test:all": "npm run test:chrome && npm run test:firefox",
@@ -48,39 +49,40 @@
"css": "postcss src/*.css --dir dist"
},
"dependencies": {
"@babel/runtime": "^7.17.9",
"@babel/runtime": "^7.18.0",
"classcat": "^5.0.3",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"react-draggable": "^4.4.5",
"zustand": "^3.7.2"
},
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/plugin-transform-runtime": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@babel/core": "^7.18.0",
"@babel/plugin-transform-runtime": "^7.18.0",
"@babel/preset-env": "^7.18.0",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.2.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@types/d3": "^7.1.0",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"@types/d3": "^7.4.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/resize-observer-browser": "^0.1.7",
"autoprefixer": "^10.4.5",
"autoprefixer": "^10.4.7",
"babel-preset-react-app": "^10.0.1",
"cypress": "^9.6.0",
"postcss": "^8.4.13",
"cross-env": "^7.0.3",
"cypress": "^9.6.1",
"postcss": "^8.4.14",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
"prettier": "^2.6.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"release-it": "^14.14.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"release-it": "^15.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.71.0",
"rollup": "^2.74.1",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-serve": "^1.1.0",
+1 -2
View File
@@ -26,7 +26,6 @@ const globals = {
classcat: 'cc',
'd3-selection': 'd3',
'd3-zoom': 'd3',
'react-draggable': 'ReactDraggable',
zustand: 'zustand',
'zustand/shallow': 'zustandShallow',
'zustand/context': 'zustandContext',
@@ -62,7 +61,7 @@ export const baseConfig = ({ outputOptions = {}, injectCSS = true } = {}) => {
'classcat',
'd3-selection',
'd3-zoom',
'react-draggable',
'd3-drag',
'zustand',
'zustand/shallow',
'zustand/context',
@@ -22,7 +22,7 @@ const Background: FC<BackgroundProps> = ({
}) => {
const ref = useRef<SVGSVGElement>(null);
const [patternId, setPatternId] = useState<string | null>(null);
const [x, y, scale] = useStore(transformSelector);
const [tX, tY, tScale] = useStore(transformSelector);
useEffect(() => {
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
@@ -31,18 +31,17 @@ const Background: FC<BackgroundProps> = ({
setPatternId(`pattern-${index}`);
}, []);
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const scaledGap = gap * tScale;
const xOffset = tX % scaledGap;
const yOffset = tY % scaledGap;
const isLines = variant === BackgroundVariant.Lines;
const bgColor = color ? color : defaultColors[variant];
const path = isLines ? createGridLinesPath(scaledGap, size, bgColor) : createGridDotsPath(size * scale, bgColor);
const path = isLines ? createGridLinesPath(scaledGap, size, bgColor) : createGridDotsPath(size * tScale, bgColor);
return (
<svg
className={bgClasses}
className={cc(['react-flow__background', 'react-flow__container', className])}
style={{
...style,
width: '100%',
+28 -30
View File
@@ -1,4 +1,4 @@
import React, { memo, useCallback, FC, useEffect, useState, PropsWithChildren } from 'react';
import React, { memo, FC, useEffect, useState, PropsWithChildren } from 'react';
import cc from 'classcat';
import { useStore, useStoreApi } from '../../store';
@@ -38,33 +38,6 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useReactFlow();
const mapClasses = cc(['react-flow__controls', className]);
const onZoomInHandler = useCallback(() => {
zoomIn?.();
onZoomIn?.();
}, [zoomIn, onZoomIn]);
const onZoomOutHandler = useCallback(() => {
zoomOut?.();
onZoomOut?.();
}, [zoomOut, onZoomOut]);
const onFitViewHandler = useCallback(() => {
fitView?.(fitViewOptions);
onFitView?.();
}, [fitView, fitViewOptions, onFitView]);
const onInteractiveChangeHandler = useCallback(() => {
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
}, [isInteractive, onInteractiveChange]);
useEffect(() => {
setIsVisible(true);
}, []);
@@ -73,8 +46,33 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
return null;
}
const onZoomInHandler = () => {
zoomIn?.();
onZoomIn?.();
};
const onZoomOutHandler = () => {
zoomOut?.();
onZoomOut?.();
};
const onFitViewHandler = () => {
fitView?.(fitViewOptions);
onFitView?.();
};
const onToggleInteractivity = () => {
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
};
return (
<div className={mapClasses} style={style}>
<div className={cc(['react-flow__controls', className])} style={style}>
{showZoom && (
<>
<ControlButton
@@ -108,7 +106,7 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
{showInteractive && (
<ControlButton
className="react-flow__controls-interactive"
onClick={onInteractiveChangeHandler}
onClick={onToggleInteractivity}
title="toggle interactivity"
aria-label="toggle interactivity"
>
+20 -32
View File
@@ -3,7 +3,6 @@ import cc from 'classcat';
import shallow from 'zustand/shallow';
import MiniMapNode from './MiniMapNode';
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
@@ -19,9 +18,11 @@ const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
nodeInternals: s.nodeInternals,
nodes: Array.from(s.nodeInternals.values()),
});
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
const MiniMap = ({
style,
className,
@@ -32,30 +33,19 @@ const MiniMap = ({
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
const [tX, tY, tScale] = transform;
const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) as GetMiniMapNodeAttribute;
const nodeClassNameFunc = (
nodeClassName instanceof Function ? nodeClassName : () => nodeClassName
) as GetMiniMapNodeAttribute;
const hasNodes = nodeInternals && nodeInternals.size > 0;
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const bb = getRectOfNodes(nodes);
const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow);
const elementWidth = (style?.width as number) ?? defaultWidth;
const elementHeight = (style?.height as number) ?? defaultHeight;
const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName);
const viewBB: Rect = {
x: -tX / tScale,
y: -tY / tScale,
width: containerWidth / tScale,
height: containerHeight / tScale,
x: -transform[0] / transform[2],
y: -transform[1] / transform[2],
width: containerWidth / transform[2],
height: containerHeight / transform[2],
};
const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB;
const boundingRect = nodes.length > 0 ? getBoundsofRects(getRectOfNodes(nodes), viewBB) : viewBB;
const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight);
@@ -74,18 +64,16 @@ const MiniMap = ({
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
style={style}
className={mapClasses}
className={cc(['react-flow__minimap', className])}
>
{Array.from(nodeInternals)
.filter(([_, node]) => !node.hidden && node.width && node.height)
.map(([_, node]) => {
const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
{nodes
.filter((node) => !node.hidden && node.width && node.height)
.map((node) => {
return (
<MiniMapNode
key={node.id}
x={positionAbsolute?.x || 0}
y={positionAbsolute?.y || 0}
x={node.positionAbsolute?.x ?? 0}
y={node.positionAbsolute?.y ?? 0}
width={node.width!}
height={node.height!}
style={node.style}
+3 -7
View File
@@ -8,14 +8,10 @@ type AttributionProps = {
position?: AttributionPosition;
};
const accounts = ['paid-pro', 'paid-sponsor', 'paid-enterprise', 'paid-custom'];
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (
(proOptions?.account === 'paid-pro' ||
proOptions?.account === 'paid-sponsor' ||
proOptions?.account === 'paid-enterprise' ||
proOptions?.account === 'paid-custom') &&
proOptions?.hideAttribution
) {
if (proOptions?.account && accounts.includes(proOptions?.account) && proOptions?.hideAttribution) {
return null;
}
+17 -1
View File
@@ -1,4 +1,7 @@
import { MarkerType, Position } from '../../types';
import { MouseEvent as ReactMouseEvent } from 'react';
import { GetState } from 'zustand';
import { Edge, MarkerType, Position, ReactFlowState } from '../../types';
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
if (typeof markerEndId !== 'undefined' && markerEndId) {
@@ -52,3 +55,16 @@ export const getCenter = ({
return [centerX, centerY, xOffset, yOffset];
};
export function getMouseHandler(
id: string,
getState: GetState<ReactFlowState>,
handler?: (event: ReactMouseEvent<SVGGElement, MouseEvent>, edge: Edge) => void
) {
return handler === undefined
? handler
: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => {
const edge = getState().edges.find((e) => e.id === id)!;
handler(event, { ...edge });
};
}
+74 -138
View File
@@ -1,12 +1,13 @@
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
import React, { memo, ComponentType, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { handleMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
@@ -53,10 +54,78 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
const store = useStoreApi();
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const [updating, setUpdating] = useState<boolean>(false);
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const store = useStoreApi();
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const edge = store.getState().edges.find((e) => e.id === id)!;
if (elementsSelectable) {
store.setState({ nodesSelectionActive: false });
addSelectedEdges([id]);
}
onClick?.(event, edge);
};
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = () => true;
const isTarget = isSourceHandle;
const edge = store.getState().edges.find((e) => e.id === id)!;
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdate = onEdgeUpdateEnd
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edge, handleType)
: undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectEdge,
isTarget,
isValidConnection,
connectionMode,
handleType,
_onEdgeUpdate,
store.getState
);
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdating(true);
const onEdgeUpdaterMouseOut = () => setUpdating(false);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
if (hidden) {
return null;
}
const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
@@ -67,139 +136,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
{ selected, animated, inactive, updating },
]);
const edgeElement = useMemo<Edge>(() => {
const el: Edge = {
id,
source,
target,
type,
};
if (sourceHandleId) {
el.sourceHandle = sourceHandleId;
}
if (targetHandleId) {
el.targetHandle = targetHandleId;
}
if (typeof data !== 'undefined') {
el.data = data;
}
return el;
}, [id, source, target, type, sourceHandleId, targetHandleId, data]);
const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) {
store.setState({ nodesSelectionActive: false });
addSelectedEdges([edgeElement.id]);
}
onClick?.(event, edgeElement);
},
[elementsSelectable, edgeElement, onClick]
);
const onEdgeDoubleClickHandler = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>) => {
onEdgeDoubleClick?.(event, edgeElement);
},
[edgeElement, onEdgeDoubleClick]
);
const onEdgeContextMenu = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
onContextMenu?.(event, edgeElement);
},
[edgeElement, onContextMenu]
);
const onEdgeMouseEnter = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
onMouseEnter?.(event, edgeElement);
},
[edgeElement, onContextMenu]
);
const onEdgeMouseMove = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
onMouseMove?.(event, edgeElement);
},
[edgeElement, onContextMenu]
);
const onEdgeMouseLeave = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
onMouseLeave?.(event, edgeElement);
},
[edgeElement, onContextMenu]
);
const handleEdgeUpdater = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = () => true;
const isTarget = isSourceHandle;
onEdgeUpdateStart?.(event, edgeElement, handleType);
const _onEdgeUpdate = onEdgeUpdateEnd
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType)
: undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectEdge,
isTarget,
isValidConnection,
connectionMode,
handleType,
_onEdgeUpdate,
store.getState
);
},
[id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
);
const onEdgeUpdaterSourceMouseDown = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
handleEdgeUpdater(event, true);
},
[id, source, sourceHandleId, handleEdgeUpdater]
);
const onEdgeUpdaterTargetMouseDown = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
handleEdgeUpdater(event, false);
},
[id, target, targetHandleId, handleEdgeUpdater]
);
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
if (hidden) {
return null;
}
return (
<g
className={edgeClasses}
+64 -86
View File
@@ -1,4 +1,4 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import React, { memo, useContext, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
@@ -18,6 +18,9 @@ const selector = (s: ReactFlowState) => ({
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
onClickConnectStart: s.onClickConnectStart,
onClickConnectStop: s.onClickConnectStop,
onClickConnectEnd: s.onClickConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
@@ -47,6 +50,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
@@ -56,99 +62,71 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = useCallback(
(params: Connection) => {
const { defaultEdgeOptions } = store.getState();
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
}
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
},
[hasDefaultEdges, onConnectAction, onConnect]
);
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const onMouseDownHandler = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (event.button === 0) {
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
}
onMouseDown?.(event);
},
[
handleId,
nodeId,
onConnectExtended,
isTarget,
isValidConnection,
const onMouseDownHandler = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.button === 0) {
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
}
onMouseDown?.(event);
};
const onClick = (event: React.MouseEvent) => {
if (!connectionStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
connectionMode,
onConnectStart,
onConnectStop,
onConnectEnd,
]
);
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
isValidConnection,
doc
);
const onClick = useCallback(
(event: React.MouseEvent) => {
if (!connectionStartHandle) {
onConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
} else {
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
isValidConnection,
doc
);
onClickConnectStop?.(event as unknown as MouseEvent);
onConnectStop?.(event as unknown as MouseEvent);
if (isValid) {
onConnectExtended(connection);
}
if (isValid) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
onConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null });
}
},
[
connectionStartHandle,
onConnectStart,
onConnectExtended,
onConnectStop,
onConnectEnd,
isTarget,
nodeId,
handleId,
type,
]
);
store.setState({ connectionStartHandle: null });
};
const handleClasses = cc([
'react-flow__handle',
@@ -1,25 +0,0 @@
import { MouseEvent, useCallback } from 'react';
import { GetState } from 'zustand';
import { ReactFlowState, Node } from '../../types';
function useMemoizedMouseHandler(
id: string,
dragging: boolean,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
const memoizedHandler = useCallback(
(event: MouseEvent) => {
if (typeof handler !== 'undefined' && !dragging) {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
}
},
[handler, dragging, id]
);
return memoizedHandler;
}
export default useMemoizedMouseHandler;
+43 -1
View File
@@ -1,4 +1,7 @@
import { HandleElement, Position } from '../../types';
import { MouseEvent } from 'react';
import { GetState, SetState } from 'zustand';
import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
@@ -39,3 +42,42 @@ export const getHandleBoundsByHandleType = (
};
});
};
export function getMouseHandler(
id: string,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
return handler === undefined
? handler
: (event: MouseEvent) => {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
};
}
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
export function handleNodeClick({
id,
store,
}: {
id: string;
store: {
getState: GetState<ReactFlowState>;
setState: SetState<ReactFlowState>;
};
}) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
const node = nodeInternals.get(id)!;
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([id]);
} else if (node.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [node] });
}
}
+86 -199
View File
@@ -1,26 +1,19 @@
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
import useDrag from '../../hooks/useDrag';
import { getMouseHandler, handleNodeClick } from './utils';
const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions,
});
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
data,
scale,
xPos,
yPos,
selected,
@@ -29,10 +22,10 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onMouseMove,
onMouseLeave,
onContextMenu,
onNodeDoubleClick,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onDoubleClick,
onDragStart,
onDrag,
onDragStop,
style,
className,
isDraggable,
@@ -42,9 +35,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
sourcePosition,
targetPosition,
hidden,
snapToGrid,
snapGrid,
dragging,
resizeObserver,
dragHandle,
zIndex,
@@ -53,142 +43,36 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noDragClassName,
}: WrapNodeProps) => {
const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
selector,
shallow
);
const nodeElement = useRef<HTMLDivElement>(null);
const updateNodeDimensions = useStore(selector);
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
...style,
}),
[zIndex, xPos, yPos, hasPointerEvents, style]
);
const grid = useMemo(
() => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
);
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
const onSelectNodeHandler = useCallback(
(event: MouseEvent) => {
if (!isDraggable) {
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedNodes([id]);
}
}
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
}
},
[isSelectable, selected, isDraggable, onClick, id]
);
const onDragStart = useCallback(
(event: DraggableEvent) => {
if (selectNodesOnDrag && isSelectable) {
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedNodes([id]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
const { multiSelectionActive } = store.getState();
if (multiSelectionActive) {
addSelectedNodes([id]);
} else {
unselectNodesAndEdges();
store.setState({ nodesSelectionActive: false });
}
}
if (onNodeDragStart) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStart(event as MouseEvent, { ...node });
}
},
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
);
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
if (onNodeDrag) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDrag(event as MouseEvent, {
...node,
dragging: true,
position: {
x: node.position.x + draggableData.deltaX,
y: node.position.y + draggableData.deltaY,
},
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
},
});
}
},
[id, onNodeDrag]
);
const onDragStop = useCallback(
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
let node;
if (onClick || onNodeDragStop) {
node = store.getState().nodeInternals.get(id)!;
}
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedNodes([id]);
}
if (onClick && node) {
onClick(event as MouseEvent, { ...node });
}
return;
}
updateNodePosition({
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave);
const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick);
const onSelectNodeHandler = (event: MouseEvent) => {
if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
// this handler gets called within the drag start event when selectNodesOnDrag=true
handleNodeClick({
id,
dragging: false,
store,
});
}
if (onNodeDragStop && node) {
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
}
},
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
);
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
};
useEffect(() => {
if (nodeElement.current && !hidden) {
const currNode = nodeElement.current;
if (nodeRef.current && !hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
@@ -201,7 +85,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
@@ -211,69 +95,72 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({
onStart: onDragStart,
onDrag: onDrag,
onStop: onDragStop,
nodeRef,
disabled: !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
selectNodesOnDrag,
});
if (hidden) {
return null;
}
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
]);
return (
<DraggableCore
onStart={onDragStart}
onDrag={onDrag}
onStop={onDragStop}
scale={scale}
disabled={!isDraggable}
cancel={`.${noDragClassName}`}
nodeRef={nodeElement}
grid={grid}
enableUserSelectHack={false}
handle={dragHandle}
<div
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
...style,
}}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
data-id={id}
>
<div
className={nodeClasses}
ref={nodeElement}
style={nodeStyle}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onNodeDoubleClickHandler}
data-id={id}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
xPos={xPos}
yPos={yPos}
selected={selected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
</DraggableCore>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
xPos={xPos}
yPos={yPos}
selected={selected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
+51 -91
View File
@@ -3,14 +3,14 @@
* made a selection with on or several nodes
*/
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import { DraggableCore, DraggableData } from 'react-draggable';
import React, { memo, useCallback, useRef, MouseEvent } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { useStore, useStoreApi } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
import useDrag from '../../hooks/useDrag';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -19,17 +19,21 @@ export interface NodesSelectionProps {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
updateNodePosition: s.updateNodePosition,
});
const bboxSelector = (s: ReactFlowState) => {
const selectedNodes = Array.from(s.nodeInternals.values()).filter((n) => n.selected);
return getRectOfNodes(selectedNodes);
};
function useGetMemoizedHandler(handler?: (event: MouseEvent, nodes: Node[]) => void) {
return useCallback((event: MouseEvent, _: Node, nodes: Node[]) => handler?.(event, nodes), [handler]);
}
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
@@ -37,96 +41,52 @@ function NodesSelection({
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
selector,
shallow
);
const [tX, tY, tScale] = transform;
const store = useStoreApi();
const { transform, userSelectionActive } = useStore(selector, shallow);
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
// it's important that these handlers are memoized to avoid multiple creation of d3 drag handler
const onStart = useGetMemoizedHandler(onSelectionDragStart);
const onDrag = useGetMemoizedHandler(onSelectionDrag);
const onStop = useGetMemoizedHandler(onSelectionDragStop);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
}),
[tX, tY, tScale]
);
useDrag({
onStart,
onDrag,
onStop,
nodeRef,
});
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
height: selectedNodesBbox.height,
top: selectedNodesBbox.y,
left: selectedNodesBbox.x,
}),
[selectedNodesBbox]
);
const onStart = useCallback(
(event: MouseEvent) => {
onSelectionDragStart?.(event, selectedNodes);
},
[onSelectionDragStart, selectedNodes]
);
const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => {
updateNodePosition({
diff: {
x: data.deltaX,
y: data.deltaY,
},
dragging: true,
});
onSelectionDrag?.(event, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosition]
);
const onStop = useCallback(
(event: MouseEvent) => {
updateNodePosition({
dragging: false,
});
onSelectionDragStop?.(event, selectedNodes);
},
[selectedNodes, onSelectionDragStop]
);
const onContextMenu = useCallback(
(event: MouseEvent) => {
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu, selectedNodes]
);
if (!selectedNodes?.length || userSelectionActive) {
if (userSelectionActive || !width || !height) {
return null;
}
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = Array.from(store.getState().nodeInternals.values()).filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
return (
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<DraggableCore
scale={tScale}
grid={grid}
onStart={(event) => onStart(event as MouseEvent)}
onDrag={(event, data) => onDrag(event as MouseEvent, data)}
onStop={(event) => onStop(event as MouseEvent)}
nodeRef={nodeRef}
enableUserSelectHack={false}
>
<div
ref={nodeRef}
className="react-flow__nodesselection-rect"
onContextMenu={onContextMenu}
style={innerStyle}
/>
</DraggableCore>
<div
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
style={{
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
}}
>
<div
ref={nodeRef}
className="react-flow__nodesselection-rect"
onContextMenu={onContextMenu}
style={{
width,
height,
top,
left,
}}
/>
</div>
);
}
+10 -1
View File
@@ -19,7 +19,7 @@ import {
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete
OnEdgesDelete,
} from '../../types';
interface StoreUpdaterProps {
@@ -31,6 +31,9 @@ interface StoreUpdaterProps {
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
@@ -88,6 +91,9 @@ const StoreUpdater = ({
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
nodesDraggable,
nodesConnectable,
minZoom,
@@ -133,6 +139,9 @@ const StoreUpdater = ({
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState);
useDirectStoreUpdater('onClickConnectStop', onClickConnectStop, store.setState);
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
+14 -16
View File
@@ -2,7 +2,7 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import React, { memo, useState, useRef, useCallback } from 'react';
import React, { memo, useState, useRef } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
@@ -52,16 +52,20 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
const resetUserSelection = useCallback(() => {
if (!elementsSelectable || !renderUserSelectionPane) {
return null;
}
const resetUserSelection = () => {
setUserSelectionRect(initialRect);
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
}, []);
};
const onMouseDown = useCallback((event: React.MouseEvent): void => {
const onMouseDown = (event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
@@ -78,7 +82,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
});
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
}, []);
};
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
@@ -98,7 +102,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const nodes = Array.from(nodeInternals.values());
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
@@ -122,21 +126,15 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
setUserSelectionRect(nextUserSelectRect);
};
const onMouseUp = useCallback(() => {
const onMouseUp = () => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
resetUserSelection();
}, []);
};
const onMouseLeave = useCallback(() => {
const onMouseLeave = () => {
store.setState({ nodesSelectionActive: false });
resetUserSelection();
}, []);
if (!elementsSelectable || !renderUserSelectionPane) {
return null;
}
};
return (
<div
+30 -15
View File
@@ -6,6 +6,8 @@ import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import {
Position,
Edge,
@@ -17,13 +19,13 @@ import {
ReactFlowState,
EdgeTypesWrapped,
} from '../../types';
import useVisibleEdges from '../../hooks/useVisibleEdges';
interface EdgeRendererProps {
edgeTypes: EdgeTypesWrapped;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionLineContainerStyle?: CSSProperties;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
defaultMarkerColor: string;
@@ -72,7 +74,13 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
return null;
}
const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
@@ -179,22 +187,29 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
/>
);
})}
{renderConnectionLine && isMaxLevel && (
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
))}
{renderConnectionLine && (
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
</>
);
};
+10 -12
View File
@@ -1,4 +1,4 @@
import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
@@ -8,6 +8,7 @@ import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import { ReactFlowState } from '../../types';
interface FlowRendererProps
@@ -18,6 +19,7 @@ interface FlowRendererProps
| 'edgeTypes'
| 'snapGrid'
| 'connectionLineType'
| 'connectionLineContainerStyle'
| 'arrowHeadColor'
| 'onlyRenderVisibleElements'
| 'selectNodesOnDrag'
@@ -67,18 +69,14 @@ const FlowRenderer = ({
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = useCallback(
(event: MouseEvent) => {
onPaneClick?.(event);
resetSelectedElements();
const onClick = (event: MouseEvent) => {
onPaneClick?.(event);
resetSelectedElements();
store.setState({ nodesSelectionActive: false });
},
[onPaneClick]
);
const onContextMenu = useCallback((event: MouseEvent) => onPaneContextMenu?.(event), [onPaneContextMenu]);
const onWheel = useCallback((event: WheelEvent) => onPaneScroll?.(event), [onPaneScroll]);
store.setState({ nodesSelectionActive: false });
};
const onContextMenu = (event: MouseEvent) => onPaneContextMenu?.(event);
const onWheel = (event: WheelEvent) => onPaneScroll?.(event);
return (
<ZoomPane
+2
View File
@@ -50,6 +50,7 @@ const GraphView = ({
connectionLineType,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
@@ -125,6 +126,7 @@ const GraphView = ({
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
+29 -29
View File
@@ -1,41 +1,43 @@
import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react';
import React, { memo, useMemo, ComponentType, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import { Node, NodeTypesWrapped, Position, ReactFlowState, WrapNodeProps } from '../../types';
import {
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
interface NodeRendererProps {
nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean;
onNodeClick?: (event: MouseEvent, element: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
onNodeMouseLeave?: (event: MouseEvent, node: Node) => void;
onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
onNodeClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
scale: s.transform[2],
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
snapGrid: s.snapGrid,
snapToGrid: s.snapToGrid,
nodeInternals: s.nodeInternals,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } =
useStore(selector, shallow);
const { nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions } = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
@@ -68,15 +70,17 @@ const NodeRenderer = (props: NodeRendererProps) => {
return (
<div className="react-flow__nodes react-flow__container">
{nodes.map((node) => {
const nodeType = node.type || 'default';
let nodeType = node.type || 'default';
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
if (!props.nodeTypes[nodeType]) {
if (!props.nodeTypes[nodeType]) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
}
nodeType = 'default';
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
@@ -97,20 +101,16 @@ const NodeRenderer = (props: NodeRendererProps) => {
hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0}
dragging={!!node.dragging}
snapGrid={snapGrid}
snapToGrid={snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu}
onNodeDoubleClick={props.onNodeDoubleClick}
onNodeDragStart={props.onNodeDragStart}
onNodeDrag={props.onNodeDrag}
onNodeDragStop={props.onNodeDragStop}
scale={scale}
onDoubleClick={props.onNodeDoubleClick}
onDragStart={props.onNodeDragStart}
onDrag={props.onNodeDrag}
onDragStop={props.onNodeDragStop}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
+8
View File
@@ -68,6 +68,9 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
@@ -87,6 +90,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
deleteKeyCode = 'Backspace',
selectionKeyCode = 'Shift',
multiSelectionKeyCode = 'Meta',
@@ -169,6 +173,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
selectionKeyCode={selectionKeyCode}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
@@ -216,6 +221,9 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
onClickConnectStart={onClickConnectStart}
onClickConnectStop={onClickConnectStop}
onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
+1 -3
View File
@@ -1,8 +1,6 @@
import { createContext } from 'react';
type ContextProps = string | null;
export const NodeIdContext = createContext<Partial<ContextProps>>(null);
export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
+176
View File
@@ -0,0 +1,176 @@
import { RefObject, useEffect, useRef, MouseEvent, useState, useCallback } from 'react';
import { D3DragEvent, drag, SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import { useStoreApi } from '../../store';
import { pointToRendererPoint } from '../../utils/graph';
import { NodeDragItem, NodeDragHandler, XYPosition } from '../../types';
import {
getDragItems,
getEventHandlerParams,
getParentNodePosition,
selectorExistsTargetToNode,
updatePosition,
} from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number };
type UseDragParams = {
nodeRef: RefObject<Element>;
onStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onStop?: NodeDragHandler;
disabled?: boolean;
noDragClassName?: string;
handleSelector?: string;
nodeId?: string;
isSelectable?: boolean;
selectNodesOnDrag?: boolean;
};
function useDrag({
onStart,
onDrag,
onStop,
nodeRef,
disabled = false,
noDragClassName,
handleSelector,
nodeId,
isSelectable,
selectNodesOnDrag,
}: UseDragParams) {
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi();
const dragItems = useRef<NodeDragItem[]>();
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
const parentPos = useRef<XYPosition>({ x: 0, y: 0 });
// returns the mouse position projected to the RF coordinate system
const getMousePosition = useCallback((event: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const mousePos = pointToRendererPoint(
{
x: event.sourceEvent.clientX,
y: event.sourceEvent.clientY,
},
transform,
snapToGrid,
snapGrid
);
mousePos.x -= parentPos.current.x;
mousePos.y -= parentPos.current.y;
return mousePos;
}, []);
useEffect(() => {
if (nodeRef?.current) {
const selection = select(nodeRef.current);
if (disabled) {
selection.on('.drag', null);
} else {
const dragHandler = drag()
.on('start', (event: UseDragEvent) => {
const { nodeInternals, multiSelectionActive, unselectNodesAndEdges } = store.getState();
parentPos.current = getParentNodePosition(nodeInternals, nodeId);
if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
if (!nodeInternals.get(nodeId)?.selected) {
// we need to reset selected nodes when selectNodesOnDrag=false
unselectNodesAndEdges();
}
}
if (nodeId && isSelectable && selectNodesOnDrag) {
handleNodeClick({
id: nodeId,
store,
});
}
const mousePos = getMousePosition(event);
dragItems.current = getDragItems(nodeInternals, mousePos, nodeId);
if (onStart && dragItems.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
}
})
.on('drag', (event: UseDragEvent) => {
const { updateNodePositions, nodeInternals, nodeExtent } = store.getState();
const mousePos = getMousePosition(event);
// skip events without movement
if ((lastPos.current.x !== mousePos.x || lastPos.current.y !== mousePos.y) && dragItems.current) {
lastPos.current = mousePos;
dragItems.current = dragItems.current.map((n) => updatePosition(n, mousePos, nodeInternals, nodeExtent));
updateNodePositions(dragItems.current);
setDragging(true);
if (onDrag) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onDrag(event.sourceEvent as MouseEvent, currentNode, nodes);
}
}
event.on('end', (event) => {
if (onStop && dragItems.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStop(event.sourceEvent as MouseEvent, currentNode, nodes);
}
});
})
.filter((event: MouseEvent) => {
const target = event.target as HTMLDivElement;
const filter =
!event.ctrlKey && !event.button && (!noDragClassName || !target.classList?.contains?.(noDragClassName));
return handleSelector
? selectorExistsTargetToNode(target as HTMLDivElement, handleSelector, nodeRef) && filter
: filter;
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}
}
}, [
onStart,
onDrag,
onStop,
nodeRef,
disabled,
noDragClassName,
handleSelector,
isSelectable,
store,
nodeId,
selectNodesOnDrag,
getMousePosition,
]);
return dragging;
}
export default useDrag;
+133
View File
@@ -0,0 +1,133 @@
import { RefObject } from 'react';
import { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types';
import { clampPosition } from '../../utils';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
export function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): XYPosition {
const parentNodeId = nodeId ? nodeInternals.get(nodeId)?.parentNode : null;
const parentNode = parentNodeId ? nodeInternals.get(parentNodeId) : null;
return {
x: parentNode?.positionAbsolute?.x || 0,
y: parentNode?.positionAbsolute?.y || 0,
};
}
export function selectorExistsTargetToNode(target: Element, selector: string, nodeRef: RefObject<Element>): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === nodeRef.current) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
// looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
return Array.from(nodeInternals.values())
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals)))
.map((n) => ({
id: n.id,
position: n.position,
distance: {
x: mousePos.x - n.position.x,
y: mousePos.y - n.position.y,
},
delta: {
x: 0,
y: 0,
},
extent: n.extent,
parentNode: n.parentNode,
width: n.width,
height: n.height,
}));
}
export function updatePosition(
dragItem: NodeDragItem,
mousePos: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent
): NodeDragItem {
let currentExtent = dragItem.extent || nodeExtent;
let nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
if (dragItem.extent === 'parent') {
if (dragItem.parentNode && dragItem.width && dragItem.height) {
const parent = nodeInternals.get(dragItem.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - dragItem.width, parent.height - dragItem.height],
]
: currentExtent;
} else {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent;
}
}
nextPosition = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
dragItem.delta = {
x: nextPosition.x - dragItem.position.x,
y: nextPosition.y - dragItem.position.y,
};
dragItem.position = nextPosition;
return dragItem;
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (handy when multi selection is active)
export function getEventHandlerParams({
nodeId,
dragItems,
nodeInternals,
}: {
nodeId?: string;
dragItems: NodeDragItem[];
nodeInternals: NodeInternals;
}): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!;
return {
...node,
position: n.position,
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + n.delta.x,
y: (node.positionAbsolute?.y || 0) + n.delta.y,
},
};
});
return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems];
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useStore } from '../store';
import { getNodesInside } from '../utils/graph';
import { ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) {
@@ -10,7 +11,7 @@ function useVisibleNodes(onlyRenderVisible: boolean) {
(s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: Array.from(s.nodeInternals).map(([_, node]) => node);
: Array.from(s.nodeInternals.values());
},
[onlyRenderVisible]
)
+47 -107
View File
@@ -3,29 +3,22 @@ import createContext from 'zustand/context';
import { clampPosition, getDimensions } from '../utils';
import { applyNodeChanges } from '../utils/changes';
import {
ReactFlowState,
Node,
Edge,
NodeDimensionUpdate,
NodeDiffUpdate,
CoordinateExtent,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
NodeDragItem,
UnselectNodesAndEdgesParams,
} from '../types';
import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
import {
createNodeInternals,
createPositionChange,
handleControlledEdgeSelectionChange,
handleControlledNodeSelectionChange,
isParentSelected,
fitView,
} from './utils';
import { createNodeInternals, fitView, updateNodesAndEdgesSelections } from './utils';
import initialState from './initialState';
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
@@ -96,20 +89,19 @@ const createStore = () =>
onNodesChange?.(changes);
}
},
updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes } = get();
updateNodePositions: (nodeDragItems: NodeDragItem[]) => {
const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) {
const changes: NodePositionChange[] = [];
nodeInternals.forEach((node) => {
if (node.selected) {
if (!node.parentNode || !isParentSelected(node, nodeInternals)) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
} else if (node.id === id) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
nodeDragItems.forEach((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
position: node.position,
};
changes.push(change);
});
if (changes?.length) {
@@ -123,17 +115,8 @@ const createStore = () =>
}
}
},
// @TODO: can we unify addSelectedNodes and addSelectedEdges somehow?
addSelectedNodes: (selectedNodeIds: string[]) => {
const {
multiSelectionActive,
onNodesChange,
nodeInternals,
hasDefaultNodes,
onEdgesChange,
hasDefaultEdges,
edges,
} = get();
const { multiSelectionActive, nodeInternals, edges } = get();
let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null;
@@ -144,32 +127,15 @@ const createStore = () =>
changedEdges = getSelectionChanges(edges, []);
}
if (changedNodes.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
if (changedEdges?.length) {
if (hasDefaultEdges) {
set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
}
onEdgesChange?.(changedEdges);
}
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
const {
multiSelectionActive,
onEdgesChange,
edges,
hasDefaultEdges,
nodeInternals,
hasDefaultNodes,
onNodesChange,
} = get();
const { multiSelectionActive, edges, nodeInternals } = get();
let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null;
@@ -180,49 +146,33 @@ const createStore = () =>
changedNodes = getSelectionChanges(Array.from(nodeInternals.values()), []);
}
if (changedEdges.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(changedEdges, edges),
});
}
onEdgesChange?.(changedEdges);
}
if (changedNodes?.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
unselectNodesAndEdges: () => {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
const nodes = Array.from(nodeInternals.values());
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { nodeInternals, edges: storeEdges } = get();
const nodesToUnselect = nodes ? nodes : Array.from(nodeInternals.values());
const edgesToUnselect = edges ? edges : storeEdges;
const nodesToUnselect = nodes.map((n) => {
const changedNodes = nodesToUnselect.map((n) => {
n.selected = false;
return createSelectionChange(n.id, false);
}) as NodeSelectionChange[];
const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[];
const changedEdges = edgesToUnselect.map((edge) =>
createSelectionChange(edge.id, false)
) as EdgeSelectionChange[];
if (nodesToUnselect.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals) });
}
onNodesChange?.(nodesToUnselect);
}
if (edgesToUnselect.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
});
}
onEdgesChange?.(edgesToUnselect);
}
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
setMinZoom: (minZoom: number) => {
const { d3Zoom, maxZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
@@ -242,7 +192,7 @@ const createStore = () =>
set({ translateExtent });
},
resetSelectedElements: () => {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
const { nodeInternals, edges } = get();
const nodes = Array.from(nodeInternals.values());
const nodesToUnselect = nodes
@@ -252,22 +202,12 @@ const createStore = () =>
.filter((e) => e.selected)
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[];
if (nodesToUnselect.length) {
if (hasDefaultNodes) {
set({
nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals),
});
}
onNodesChange?.(nodesToUnselect);
}
if (edgesToUnselect.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
});
}
onEdgesChange?.(edgesToUnselect);
}
updateNodesAndEdgesSelections({
changedNodes: nodesToUnselect,
changedEdges: edgesToUnselect,
get,
set,
});
},
setNodeExtent: (nodeExtent: CoordinateExtent) => {
const { nodeInternals } = get();
-1
View File
@@ -15,7 +15,6 @@ const initialState: ReactFlowStore = {
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
+30 -74
View File
@@ -1,18 +1,15 @@
import { zoomIdentity } from 'd3-zoom';
import { GetState } from 'zustand';
import { GetState, SetState } from 'zustand';
import { clampPosition, isNumeric } from '../utils';
import { isNumeric } from '../utils';
import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph';
import {
CoordinateExtent,
Edge,
EdgeSelectionChange,
Node,
NodeInternals,
NodePositionChange,
NodeSelectionChange,
ReactFlowState,
XYPosition,
XYZPosition,
FitViewOptions,
} from '../types';
@@ -42,7 +39,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
const parentNodes: ParentNodes = {};
nodes.forEach((node) => {
const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0;
const z = isNumeric(node.zIndex) ? node.zIndex : node.selected ? 1000 : 0;
const currInternals = nodeInternals.get(node.id);
const internals: Node = {
@@ -90,74 +87,6 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals;
}
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
type CreatePostionChangeParams = {
node: Node;
nodeExtent: CoordinateExtent;
nodeInternals: NodeInternals;
diff?: XYPosition;
dragging?: boolean;
};
export function createPositionChange({
node,
diff,
dragging,
nodeExtent,
nodeInternals,
}: CreatePostionChangeParams): NodePositionChange {
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging: !!dragging,
};
if (diff) {
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y };
let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - node.width, parent.height - node.height],
]
: currentExtent;
} else {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent;
}
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
}
return change;
}
type InternalFitViewOptions = {
initial?: boolean;
} & FitViewOptions;
@@ -223,3 +152,30 @@ export function handleControlledEdgeSelectionChange(edgeChanges: EdgeSelectionCh
return e;
});
}
type UpdateNodesAndEdgesParams = {
changedNodes: NodeSelectionChange[] | null;
changedEdges: EdgeSelectionChange[] | null;
get: GetState<ReactFlowState>;
set: SetState<ReactFlowState>;
};
export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get, set }: UpdateNodesAndEdgesParams) {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
if (changedNodes?.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
if (changedEdges?.length) {
if (hasDefaultEdges) {
set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
}
onEdgesChange?.(changedEdges);
}
}
+4
View File
@@ -36,6 +36,10 @@
overflow: visible;
}
.react-flow .react-flow__connectionline {
z-index: 1001;
}
.react-flow__edge {
pointer-events: visibleStroke;
+5 -7
View File
@@ -54,8 +54,7 @@
background: #fff;
border-color: #1a192b;
&.selected,
&.selected:hover {
&.selected {
box-shadow: 0 0 0 0.5px #1a192b;
}
@@ -71,16 +70,15 @@
&:hover {
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
}
&.selected {
box-shadow: 0 0 0 0.5px #1a192b;
}
}
.react-flow__node-group {
background: rgba(240, 240, 240, 0.25);
border-color: #1a192b;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #1a192b;
}
}
.react-flow__nodesselection-rect,
-1
View File
@@ -13,7 +13,6 @@ export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
dragging?: boolean;
};
export type NodeSelectionChange = {
+15 -9
View File
@@ -36,6 +36,8 @@ import {
OnMove,
OnMoveStart,
OnMoveEnd,
NodeDragHandler,
NodeMouseHandler,
} from '.';
import { HandleType } from './handles';
@@ -47,22 +49,25 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
onNodeClick?: (event: React.MouseEvent, node: Node) => void;
onNodeClick?: NodeMouseHandler;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: OnConnectEnd;
onInit?: OnInit;
onMove?: OnMove;
onMoveStart?: OnMoveStart;
@@ -81,6 +86,7 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionLineContainerStyle?: CSSProperties;
deleteKeyCode?: KeyCode | null;
selectionKeyCode?: KeyCode | null;
multiSelectionKeyCode?: KeyCode | null;
+12 -4
View File
@@ -3,7 +3,7 @@ import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate, NodeProps, WrapNodeProps } from './nodes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeProps, WrapNodeProps, NodeDragItem } from './nodes';
import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
@@ -105,6 +105,11 @@ export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
export type UnselectNodesAndEdgesParams = {
nodes?: Node[];
edges?: Edge[];
};
export interface ViewportHelperFunctions {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
@@ -125,7 +130,6 @@ export type ReactFlowStore = {
transform: Transform;
nodeInternals: NodeInternals;
edges: Edge[];
selectedNodesBbox: Rect;
onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean;
@@ -166,6 +170,10 @@ export type ReactFlowStore = {
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: OnConnectEnd;
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
@@ -182,9 +190,9 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void;
updateNodePositions: (nodeDragItems: NodeDragItem[]) => void;
resetSelectedElements: () => void;
unselectNodesAndEdges: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void;
+19 -16
View File
@@ -1,6 +1,5 @@
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { SnapGrid } from './general';
import { XYPosition, Position, CoordinateExtent } from './utils';
import { HandleElement } from './handles';
@@ -16,7 +15,6 @@ export interface Node<T = any> {
sourcePosition?: Position;
hidden?: boolean;
selected?: boolean;
dragging?: boolean;
draggable?: boolean;
selectable?: boolean;
connectable?: boolean;
@@ -52,6 +50,7 @@ export interface NodeProps<T = any> {
}
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export interface WrapNodeProps<T = any> {
id: string;
@@ -59,7 +58,6 @@ export interface WrapNodeProps<T = any> {
data: T;
selected: boolean;
isConnectable: boolean;
scale: number;
xPos: number;
yPos: number;
width?: number | null;
@@ -68,22 +66,19 @@ export interface WrapNodeProps<T = any> {
isDraggable: boolean;
selectNodesOnDrag: boolean;
onClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeMouseHandler;
onNodeDrag?: NodeMouseHandler;
onNodeDragStop?: NodeMouseHandler;
onDragStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onDragStop?: NodeDragHandler;
style?: CSSProperties;
className?: string;
sourcePosition: Position;
targetPosition: Position;
hidden?: boolean;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
dragging: boolean;
resizeObserver: ResizeObserver | null;
dragHandle?: string;
zIndex: number;
@@ -97,12 +92,6 @@ export type NodeHandleBounds = {
target: HandleElement[] | null;
};
export type NodeDiffUpdate = {
id?: string;
diff?: XYPosition;
dragging?: boolean;
};
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
@@ -115,3 +104,17 @@ export type NodeBounds = XYPosition & {
width: number | null;
height: number | null;
};
export type NodeDragItem = {
id: string;
// relative node position
position: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
// delta to previous position
delta: XYPosition;
width?: number | null;
height?: number | null;
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
};
+1 -6
View File
@@ -45,8 +45,7 @@ function handleParentExpand(res: any[], updateItem: any) {
}
function applyChanges(changes: any[], elements: any[]): any[] {
// unfortunately we need this hack to handle the setNodes and setEdges function of the
// useReactFlow hook.
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
if (changes.some((c) => c.type === 'reset')) {
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
}
@@ -69,10 +68,6 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updateItem.position = currentChange.position;
}
if (typeof currentChange.dragging !== 'undefined') {
updateItem.dragging = currentChange.dragging;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
+2 -2
View File
@@ -176,7 +176,7 @@ export const getNodesInside = (
const visibleNodes: Node[] = [];
nodeInternals.forEach((node) => {
const { positionAbsolute, width, height, dragging, selectable = true } = node;
const { positionAbsolute, width, height, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) {
return false;
@@ -187,7 +187,7 @@ export const getNodesInside = (
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging;
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
const partiallyVisible = partially && overlappingArea > 0;
const area = (width || 0) * (height || 0);