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