Merge pull request #2152 from wbkd/next-release/v10.3
Next release/v10.3
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 })
|
||||
);
|
||||
});
|
||||
|
||||
Generated
+4116
-6710
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ const BasicFlow = () => {
|
||||
maxZoom={4}
|
||||
fitView
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
selectNodesOnDrag={false}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -86,8 +86,8 @@ const UpdatableEdge = () => {
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
// onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
// onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
||||
Generated
+1982
-1335
File diff suppressed because it is too large
Load Diff
+22
-20
@@ -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
@@ -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%',
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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,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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.react-flow .react-flow__connectionline {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.react-flow__edge {
|
||||
pointer-events: visibleStroke;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,7 +13,6 @@ export type NodePositionChange = {
|
||||
id: string;
|
||||
type: 'position';
|
||||
position?: XYPosition;
|
||||
dragging?: boolean;
|
||||
};
|
||||
|
||||
export type NodeSelectionChange = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user