@@ -14,4 +14,12 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands'
|
import './commands';
|
||||||
|
|
||||||
|
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
||||||
|
|
||||||
|
Cypress.on('uncaught:exception', (err) => {
|
||||||
|
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Generated
+49
-26883
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@craco/craco": "^5.7.0",
|
"@craco/craco": "^5.7.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.1",
|
||||||
"react-flow-renderer": "file:..",
|
"react-flow-renderer": "file:..",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^3.4.4"
|
"react-scripts": "^3.4.4"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
|||||||
|
|
||||||
import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer';
|
import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer';
|
||||||
|
|
||||||
const onLoad = (reactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
|
|
||||||
const onNodeDragStop = (event, node) => console.log('drag stop', node);
|
const onNodeDragStop = (event, node) => console.log('drag stop', node);
|
||||||
const onElementClick = (event, element) => console.log('click', element);
|
const onElementClick = (event, element) => console.log('click', element);
|
||||||
|
|
||||||
@@ -16,9 +15,11 @@ const initialElements = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const BasicFlow = () => {
|
const BasicFlow = () => {
|
||||||
|
const [rfInstance, setRfInstance] = useState(null);
|
||||||
const [elements, setElements] = useState(initialElements);
|
const [elements, setElements] = useState(initialElements);
|
||||||
const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
|
const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
|
||||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||||
|
const onLoad = (reactFlowInstance) => setRfInstance(reactFlowInstance);
|
||||||
|
|
||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
setElements((elms) => {
|
setElements((elms) => {
|
||||||
@@ -38,6 +39,10 @@ const BasicFlow = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logToObject = () => console.log(rfInstance.toObject());
|
||||||
|
|
||||||
|
const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
elements={elements}
|
elements={elements}
|
||||||
@@ -53,9 +58,15 @@ const BasicFlow = () => {
|
|||||||
>
|
>
|
||||||
<Background variant="lines" />
|
<Background variant="lines" />
|
||||||
|
|
||||||
<button onClick={updatePos} style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}>
|
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||||
change pos
|
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||||
</button>
|
reset transform
|
||||||
|
</button>
|
||||||
|
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||||
|
change pos
|
||||||
|
</button>
|
||||||
|
<button onClick={logToObject}>toObject</button>
|
||||||
|
</div>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const onElementClick = (event, element) => console.log('click', element);
|
|||||||
const onPaneClick = (event) => console.log('onPaneClick', event);
|
const onPaneClick = (event) => console.log('onPaneClick', event);
|
||||||
const onPaneScroll = (event) => console.log('onPaneScroll', event);
|
const onPaneScroll = (event) => console.log('onPaneScroll', event);
|
||||||
const onPaneContextMenu = (event) => console.log('onPaneContextMenu', event);
|
const onPaneContextMenu = (event) => console.log('onPaneContextMenu', event);
|
||||||
|
const onMoveEnd = (event) => console.log('onMoveEnd', event);
|
||||||
|
|
||||||
const InteractionFlow = () => {
|
const InteractionFlow = () => {
|
||||||
const [elements, setElements] = useState(initialElements);
|
const [elements, setElements] = useState(initialElements);
|
||||||
@@ -27,6 +28,7 @@ const InteractionFlow = () => {
|
|||||||
const [isConnectable, setIsConnectable] = useState(false);
|
const [isConnectable, setIsConnectable] = useState(false);
|
||||||
const [zoomOnScroll, setZoomOnScroll] = useState(false);
|
const [zoomOnScroll, setZoomOnScroll] = useState(false);
|
||||||
const [panOnScroll, setPanOnScroll] = useState(false);
|
const [panOnScroll, setPanOnScroll] = useState(false);
|
||||||
|
const [panOnScrollMode, setPanOnScrollMode] = useState('free');
|
||||||
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false);
|
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false);
|
||||||
const [paneMoveable, setPaneMoveable] = useState(true);
|
const [paneMoveable, setPaneMoveable] = useState(true);
|
||||||
const [captureZoomClick, setCaptureZoomClick] = useState(false);
|
const [captureZoomClick, setCaptureZoomClick] = useState(false);
|
||||||
@@ -41,6 +43,7 @@ const InteractionFlow = () => {
|
|||||||
nodesDraggable={isDraggable}
|
nodesDraggable={isDraggable}
|
||||||
zoomOnScroll={zoomOnScroll}
|
zoomOnScroll={zoomOnScroll}
|
||||||
panOnScroll={panOnScroll}
|
panOnScroll={panOnScroll}
|
||||||
|
panOnScrollMode={panOnScrollMode}
|
||||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onElementClick={captureElementClick ? onElementClick : undefined}
|
onElementClick={captureElementClick ? onElementClick : undefined}
|
||||||
@@ -50,6 +53,7 @@ const InteractionFlow = () => {
|
|||||||
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
||||||
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
||||||
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
||||||
|
onMoveEnd={onMoveEnd}
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
@@ -115,6 +119,21 @@ const InteractionFlow = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="panonscrollmode">
|
||||||
|
panOnScrollMode
|
||||||
|
<select
|
||||||
|
id="panonscrollmode"
|
||||||
|
value={panOnScrollMode}
|
||||||
|
onChange={(event) => setPanOnScrollMode(event.target.value)}
|
||||||
|
className="react-flow__panonscrollmode"
|
||||||
|
>
|
||||||
|
<option value="free">free</option>
|
||||||
|
<option value="horizontal">horizontal</option>
|
||||||
|
<option value="vertical">vertical</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="zoomondbl">
|
<label htmlFor="zoomondbl">
|
||||||
zoomOnDoubleClick
|
zoomOnDoubleClick
|
||||||
|
|||||||
Generated
+363
-490
File diff suppressed because it is too large
Load Diff
+10
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-flow-renderer",
|
"name": "react-flow-renderer",
|
||||||
"version": "7.1.2",
|
"version": "8.0.0-next.8",
|
||||||
"main": "dist/ReactFlow.js",
|
"main": "dist/ReactFlow.js",
|
||||||
"module": "dist/ReactFlow.esm.js",
|
"module": "dist/ReactFlow.esm.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -23,18 +23,16 @@
|
|||||||
"test:all": "npm run test:chrome && npm run test:firefox",
|
"test:all": "npm run test:chrome && npm run test:firefox",
|
||||||
"test": "npm run dev:wait test:chrome",
|
"test": "npm run dev:wait test:chrome",
|
||||||
"release": "npm run test && release-it",
|
"release": "npm run test && release-it",
|
||||||
"release:next": "release-it major --preRelease=next"
|
"release:next": "release-it --preRelease=next"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@welldone-software/why-did-you-render": "^5.0.0",
|
|
||||||
"classcat": "^4.1.0",
|
"classcat": "^4.1.0",
|
||||||
"d3-selection": "^2.0.0",
|
"d3-selection": "^2.0.0",
|
||||||
"d3-zoom": "^2.0.0",
|
"d3-zoom": "^2.0.0",
|
||||||
"easy-peasy": "^4.0.1",
|
"easy-peasy": "^4.0.1",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"react-draggable": "^4.4.3",
|
"react-draggable": "^4.4.3"
|
||||||
"resize-observer": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.3",
|
"@babel/core": "^7.12.3",
|
||||||
@@ -45,20 +43,21 @@
|
|||||||
"@rollup/plugin-commonjs": "^16.0.0",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@rollup/plugin-replace": "^2.3.4",
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"@svgr/rollup": "^5.4.0",
|
"@svgr/rollup": "^5.5.0",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/d3": "^6.0.0",
|
"@types/d3": "^6.1.0",
|
||||||
"@types/react": "^16.9.56",
|
"@types/react": "^16.9.56",
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^16.9.9",
|
||||||
|
"@types/resize-observer-browser": "^0.1.4",
|
||||||
"autoprefixer": "^9.8.6",
|
"autoprefixer": "^9.8.6",
|
||||||
"babel-preset-react-app": "^10.0.0",
|
"babel-preset-react-app": "^10.0.0",
|
||||||
"cypress": "^5.5.0",
|
"cypress": "^5.6.0",
|
||||||
"postcss-nested": "^4.2.3",
|
"postcss-nested": "^4.2.3",
|
||||||
"prettier": "2.1.2",
|
"prettier": "2.1.2",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.1",
|
||||||
"release-it": "^14.2.1",
|
"release-it": "^14.2.1",
|
||||||
"rollup": "^2.33.1",
|
"rollup": "^2.33.2",
|
||||||
"rollup-plugin-bundle-size": "^1.0.3",
|
"rollup-plugin-bundle-size": "^1.0.3",
|
||||||
"rollup-plugin-postcss": "^3.1.8",
|
"rollup-plugin-postcss": "^3.1.8",
|
||||||
"rollup-plugin-typescript2": "^0.29.0",
|
"rollup-plugin-typescript2": "^0.29.0",
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.14.0"
|
"react": "16 || 17"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo, useCallback } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||||
@@ -10,6 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg';
|
|||||||
import UnlockIcon from '../../../assets/icons/unlock.svg';
|
import UnlockIcon from '../../../assets/icons/unlock.svg';
|
||||||
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
|
||||||
|
|
||||||
export interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
showZoom?: boolean;
|
showZoom?: boolean;
|
||||||
@@ -33,57 +34,52 @@ const Controls = ({
|
|||||||
className,
|
className,
|
||||||
}: ControlProps) => {
|
}: ControlProps) => {
|
||||||
const setInteractive = useStoreActions((actions) => actions.setInteractive);
|
const setInteractive = useStoreActions((actions) => actions.setInteractive);
|
||||||
const fitView = useStoreActions((actions) => actions.fitView);
|
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
|
||||||
const zoomIn = useStoreActions((actions) => actions.zoomIn);
|
|
||||||
const zoomOut = useStoreActions((actions) => actions.zoomOut);
|
|
||||||
|
|
||||||
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
|
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
|
||||||
const mapClasses = cc(['react-flow__controls', className]);
|
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?.();
|
||||||
|
onFitView?.();
|
||||||
|
}, [fitView, onFitView]);
|
||||||
|
|
||||||
|
const onInteractiveChangeHandler = useCallback(() => {
|
||||||
|
setInteractive?.(!isInteractive);
|
||||||
|
onInteractiveChange?.(!isInteractive);
|
||||||
|
}, [isInteractive, setInteractive, onInteractiveChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={mapClasses} style={style}>
|
<div className={mapClasses} style={style}>
|
||||||
{showZoom && (
|
{showZoom && (
|
||||||
<>
|
<>
|
||||||
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={() => {
|
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={onZoomInHandler}>
|
||||||
zoomIn();
|
|
||||||
if (onZoomIn) {
|
|
||||||
onZoomIn();
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={() => {
|
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={onZoomOutHandler}>
|
||||||
zoomOut();
|
|
||||||
if (onZoomOut) {
|
|
||||||
onZoomOut();
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showFitView && (
|
{showFitView && (
|
||||||
<div
|
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={onFitViewHandler}>
|
||||||
className="react-flow__controls-button react-flow__controls-fitview"
|
|
||||||
onClick={() => {
|
|
||||||
fitView({ padding: 0.1 });
|
|
||||||
if (onFitView) {
|
|
||||||
onFitView();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FitviewIcon />
|
<FitviewIcon />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showInteractive && (
|
{showInteractive && (
|
||||||
<div
|
<div
|
||||||
className="react-flow__controls-button react-flow__controls-interactive"
|
className="react-flow__controls-button react-flow__controls-interactive"
|
||||||
onClick={() => {
|
onClick={onInteractiveChangeHandler}
|
||||||
setInteractive(!isInteractive);
|
|
||||||
if (onInteractiveChange) {
|
|
||||||
onInteractiveChange(!isInteractive);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const MiniMap = ({
|
|||||||
nodeColor = '#fff',
|
nodeColor = '#fff',
|
||||||
nodeClassName = '',
|
nodeClassName = '',
|
||||||
nodeBorderRadius = 5,
|
nodeBorderRadius = 5,
|
||||||
maskColor = '#F0F2F3',
|
maskColor = 'rgb(240, 242, 243, 0.7)',
|
||||||
}: MiniMapProps) => {
|
}: MiniMapProps) => {
|
||||||
const containerWidth = useStoreState((s) => s.width);
|
const containerWidth = useStoreState((s) => s.width);
|
||||||
const containerHeight = useStoreState((s) => s.height);
|
const containerHeight = useStoreState((s) => s.height);
|
||||||
|
|||||||
@@ -8,30 +8,18 @@ import { EdgeSmoothStepProps, Position } from '../../types';
|
|||||||
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
||||||
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
|
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
|
||||||
// We have to consider the direction of the paths because of the animated lines.
|
// We have to consider the direction of the paths because of the animated lines.
|
||||||
|
const bottomLeftCorner = (x: number, y: number, size: number): string =>
|
||||||
const bottomLeftCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
`L ${x},${y - size}Q ${x},${y} ${x + size},${y}`;
|
||||||
`L ${cornerX},${cornerY - cornerSize}Q ${cornerX},${cornerY} ${cornerX + cornerSize},${cornerY}`;
|
const leftBottomCorner = (x: number, y: number, size: number): string =>
|
||||||
|
`L ${x + size},${y}Q ${x},${y} ${x},${y - size}`;
|
||||||
const leftBottomCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
const bottomRightCorner = (x: number, y: number, size: number): string =>
|
||||||
`L ${cornerX + cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY - cornerSize}`;
|
`L ${x},${y - size}Q ${x},${y} ${x - size},${y}`;
|
||||||
|
const rightBottomCorner = (x: number, y: number, size: number): string =>
|
||||||
const bottomRightCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
`L ${x - size},${y}Q ${x},${y} ${x},${y - size}`;
|
||||||
`L ${cornerX},${cornerY - cornerSize}Q ${cornerX},${cornerY} ${cornerX - cornerSize},${cornerY}`;
|
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`;
|
||||||
|
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`;
|
||||||
const rightBottomCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
|
||||||
`L ${cornerX - cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY - cornerSize}`;
|
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
|
||||||
|
|
||||||
const leftTopCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
|
||||||
`L ${cornerX + cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY + cornerSize}`;
|
|
||||||
|
|
||||||
const topLeftCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
|
||||||
`L ${cornerX},${cornerY + cornerSize}Q ${cornerX},${cornerY} ${cornerX + cornerSize},${cornerY}`;
|
|
||||||
|
|
||||||
const topRightCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
|
||||||
`L ${cornerX},${cornerY + cornerSize}Q ${cornerX},${cornerY} ${cornerX - cornerSize},${cornerY}`;
|
|
||||||
|
|
||||||
const rightTopCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
|
|
||||||
`L ${cornerX - cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY + cornerSize}`;
|
|
||||||
|
|
||||||
interface GetSmoothStepPathParams {
|
interface GetSmoothStepPathParams {
|
||||||
sourceX: number;
|
sourceX: number;
|
||||||
@@ -60,27 +48,12 @@ export function getSmoothStepPath({
|
|||||||
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX));
|
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX));
|
||||||
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY));
|
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY));
|
||||||
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY);
|
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY);
|
||||||
|
|
||||||
const leftAndRight = [Position.Left, Position.Right];
|
const leftAndRight = [Position.Left, Position.Right];
|
||||||
|
|
||||||
let firstCornerPath = null;
|
|
||||||
let secondCornerPath = null;
|
|
||||||
|
|
||||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
||||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
|
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
|
||||||
|
|
||||||
// default case: source and target positions are top or bottom
|
let firstCornerPath = null;
|
||||||
if (sourceX <= targetX) {
|
let secondCornerPath = null;
|
||||||
firstCornerPath =
|
|
||||||
sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize);
|
|
||||||
secondCornerPath =
|
|
||||||
sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize);
|
|
||||||
} else {
|
|
||||||
firstCornerPath =
|
|
||||||
sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize);
|
|
||||||
secondCornerPath =
|
|
||||||
sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||||
if (sourceX <= targetX) {
|
if (sourceX <= targetX) {
|
||||||
@@ -115,6 +88,18 @@ export function getSmoothStepPath({
|
|||||||
: topRightCorner(sourceX, targetY, cornerSize);
|
: topRightCorner(sourceX, targetY, cornerSize);
|
||||||
}
|
}
|
||||||
secondCornerPath = '';
|
secondCornerPath = '';
|
||||||
|
} else {
|
||||||
|
if (sourceX <= targetX) {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize);
|
||||||
|
secondCornerPath =
|
||||||
|
sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize);
|
||||||
|
} else {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize);
|
||||||
|
secondCornerPath =
|
||||||
|
sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`;
|
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`;
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
import { useStoreState, useStoreActions } from '../store/hooks';
|
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||||
import { parseElement, isNode, isEdge } from '../utils/graph';
|
import { parseElement, isNode, isEdge } from '../../utils/graph';
|
||||||
import { Elements, Node, Edge, FlowElement } from '../types';
|
import { Elements, Node, Edge, FlowElement } from '../../types';
|
||||||
|
|
||||||
const useElementUpdater = (propElements: Elements): void => {
|
interface ElementUpdaterProps {
|
||||||
|
elements: Elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
|
||||||
const stateElements = useStoreState((state) => state.elements);
|
const stateElements = useStoreState((state) => state.elements);
|
||||||
const setElements = useStoreActions((actions) => actions.setElements);
|
const setElements = useStoreActions((actions) => actions.setElements);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextElements: Elements = propElements.map((propElement) => {
|
const nextElements: Elements = elements.map((propElement) => {
|
||||||
const existingElement = stateElements.find((el) => el.id === propElement.id?.toString());
|
const existingElement = stateElements.find((el) => el.id === propElement.id?.toString());
|
||||||
|
|
||||||
if (existingElement) {
|
if (existingElement) {
|
||||||
@@ -137,7 +141,9 @@ const useElementUpdater = (propElements: Elements): void => {
|
|||||||
if (elementsChanged) {
|
if (elementsChanged) {
|
||||||
setElements(nextElements);
|
setElements(nextElements);
|
||||||
}
|
}
|
||||||
}, [propElements, stateElements]);
|
}, [elements, stateElements]);
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useElementUpdater;
|
export default ElementUpdater;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
|
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
|
||||||
import { DraggableCore } from 'react-draggable';
|
import { DraggableCore } from 'react-draggable';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { ResizeObserver } from 'resize-observer';
|
|
||||||
|
|
||||||
import { useStoreActions } from '../../store/hooks';
|
import { useStoreActions } from '../../store/hooks';
|
||||||
import { Provider } from '../../contexts/NodeIdContext';
|
import { Provider } from '../../contexts/NodeIdContext';
|
||||||
@@ -12,7 +11,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
transform,
|
scale,
|
||||||
xPos,
|
xPos,
|
||||||
yPos,
|
yPos,
|
||||||
selected,
|
selected,
|
||||||
@@ -199,7 +198,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
onStart={onDragStart}
|
onStart={onDragStart}
|
||||||
onDrag={onDrag}
|
onDrag={onDrag}
|
||||||
onStop={onDragStop}
|
onStop={onDragStop}
|
||||||
scale={transform[2]}
|
scale={scale}
|
||||||
disabled={!isDraggable}
|
disabled={!isDraggable}
|
||||||
cancel=".nodrag"
|
cancel=".nodrag"
|
||||||
nodeRef={nodeElement}
|
nodeRef={nodeElement}
|
||||||
|
|||||||
@@ -4,17 +4,8 @@ import { useStoreState } from '../../store/hooks';
|
|||||||
import ConnectionLine from '../../components/ConnectionLine/index';
|
import ConnectionLine from '../../components/ConnectionLine/index';
|
||||||
import { isEdge } from '../../utils/graph';
|
import { isEdge } from '../../utils/graph';
|
||||||
import MarkerDefinitions from './MarkerDefinitions';
|
import MarkerDefinitions from './MarkerDefinitions';
|
||||||
import {
|
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils';
|
||||||
XYPosition,
|
import { Position, Edge, Node, Elements, ConnectionLineType, ConnectionLineComponent, Transform } from '../../types';
|
||||||
Position,
|
|
||||||
Edge,
|
|
||||||
Node,
|
|
||||||
ElementId,
|
|
||||||
HandleElement,
|
|
||||||
Elements,
|
|
||||||
ConnectionLineType,
|
|
||||||
ConnectionLineComponent,
|
|
||||||
} from '../../types';
|
|
||||||
|
|
||||||
interface EdgeRendererProps {
|
interface EdgeRendererProps {
|
||||||
edgeTypes: any;
|
edgeTypes: any;
|
||||||
@@ -24,109 +15,7 @@ interface EdgeRendererProps {
|
|||||||
arrowHeadColor: string;
|
arrowHeadColor: string;
|
||||||
markerEndId?: string;
|
markerEndId?: string;
|
||||||
connectionLineComponent?: ConnectionLineComponent;
|
connectionLineComponent?: ConnectionLineComponent;
|
||||||
}
|
onlyRenderVisibleElements: boolean;
|
||||||
|
|
||||||
interface EdgePositions {
|
|
||||||
sourceX: number;
|
|
||||||
sourceY: number;
|
|
||||||
targetX: number;
|
|
||||||
targetY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
|
|
||||||
if (!handle) {
|
|
||||||
switch (position) {
|
|
||||||
case Position.Top:
|
|
||||||
return {
|
|
||||||
x: node.__rf.width / 2,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
case Position.Right:
|
|
||||||
return {
|
|
||||||
x: node.__rf.width,
|
|
||||||
y: node.__rf.height / 2,
|
|
||||||
};
|
|
||||||
case Position.Bottom:
|
|
||||||
return {
|
|
||||||
x: node.__rf.width / 2,
|
|
||||||
y: node.__rf.height,
|
|
||||||
};
|
|
||||||
case Position.Left:
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: node.__rf.height / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case Position.Top:
|
|
||||||
return {
|
|
||||||
x: handle.x + handle.width / 2,
|
|
||||||
y: handle.y,
|
|
||||||
};
|
|
||||||
case Position.Right:
|
|
||||||
return {
|
|
||||||
x: handle.x + handle.width,
|
|
||||||
y: handle.y + handle.height / 2,
|
|
||||||
};
|
|
||||||
case Position.Bottom:
|
|
||||||
return {
|
|
||||||
x: handle.x + handle.width / 2,
|
|
||||||
y: handle.y + handle.height,
|
|
||||||
};
|
|
||||||
case Position.Left:
|
|
||||||
return {
|
|
||||||
x: handle.x,
|
|
||||||
y: handle.y + handle.height / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null | undefined {
|
|
||||||
let handle = null;
|
|
||||||
|
|
||||||
if (!bounds) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// there is no handleId when there are no multiple handles/ handles with ids
|
|
||||||
// so we just pick the first one
|
|
||||||
if (bounds.length === 1 || !handleId) {
|
|
||||||
handle = bounds[0];
|
|
||||||
} else if (handleId) {
|
|
||||||
handle = bounds.find((d) => d.id === handleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof handle === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEdgePositions(
|
|
||||||
sourceNode: Node,
|
|
||||||
sourceHandle: HandleElement | unknown,
|
|
||||||
sourcePosition: Position,
|
|
||||||
targetNode: Node,
|
|
||||||
targetHandle: HandleElement | unknown,
|
|
||||||
targetPosition: Position
|
|
||||||
): EdgePositions {
|
|
||||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
|
|
||||||
const sourceX = sourceNode.__rf.position.x + sourceHandlePos.x;
|
|
||||||
const sourceY = sourceNode.__rf.position.y + sourceHandlePos.y;
|
|
||||||
|
|
||||||
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
|
|
||||||
const targetX = targetNode.__rf.position.x + targetHandlePos.x;
|
|
||||||
const targetY = targetNode.__rf.position.y + targetHandlePos.y;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEdge(
|
function renderEdge(
|
||||||
@@ -134,23 +23,23 @@ function renderEdge(
|
|||||||
props: EdgeRendererProps,
|
props: EdgeRendererProps,
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
selectedElements: Elements | null,
|
selectedElements: Elements | null,
|
||||||
elementsSelectable: boolean
|
elementsSelectable: boolean,
|
||||||
|
transform: Transform,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
onlyRenderVisibleElements: boolean
|
||||||
) {
|
) {
|
||||||
const sourceId = edge.source;
|
|
||||||
const sourceHandleId = edge.sourceHandle || null;
|
const sourceHandleId = edge.sourceHandle || null;
|
||||||
const targetId = edge.target;
|
|
||||||
const targetHandleId = edge.targetHandle || null;
|
const targetHandleId = edge.targetHandle || null;
|
||||||
|
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
|
||||||
const sourceNode = nodes.find((n) => n.id === sourceId);
|
|
||||||
const targetNode = nodes.find((n) => n.id === targetId);
|
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
console.warn(`couldn't create edge for source id: ${sourceId}`);
|
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
console.warn(`couldn't create edge for target id: ${targetId}`);
|
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +55,12 @@ function renderEdge(
|
|||||||
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
|
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
|
||||||
|
|
||||||
if (!sourceHandle) {
|
if (!sourceHandle) {
|
||||||
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}`);
|
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetHandle) {
|
if (!targetHandle) {
|
||||||
console.warn(`couldn't create edge for source handle id: ${targetHandleId}`);
|
console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +73,21 @@ function renderEdge(
|
|||||||
targetPosition
|
targetPosition
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelected = selectedElements ? selectedElements.some((elm) => isEdge(elm) && elm.id === edge.id) : false;
|
const isVisible = onlyRenderVisibleElements
|
||||||
|
? isEdgeVisible({
|
||||||
|
sourcePos: { x: sourceX, y: sourceY },
|
||||||
|
targetPos: { x: targetX, y: targetY },
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
transform,
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
@@ -222,9 +125,8 @@ function renderEdge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EdgeRenderer = (props: EdgeRendererProps) => {
|
const EdgeRenderer = (props: EdgeRendererProps) => {
|
||||||
const [tX, tY, tScale] = useStoreState((state) => state.transform);
|
const transform = useStoreState((state) => state.transform);
|
||||||
const edges = useStoreState((state) => state.edges);
|
const edges = useStoreState((state) => state.edges);
|
||||||
const nodes = useStoreState((state) => state.nodes);
|
|
||||||
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
|
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
|
||||||
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
|
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
|
||||||
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
|
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
|
||||||
@@ -234,21 +136,39 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
|
|||||||
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
||||||
const width = useStoreState((state) => state.width);
|
const width = useStoreState((state) => state.width);
|
||||||
const height = useStoreState((state) => state.height);
|
const height = useStoreState((state) => state.height);
|
||||||
|
const nodes = useStoreState((state) => state.nodes);
|
||||||
const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props;
|
|
||||||
|
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformStyle = `translate(${tX},${tY}) scale(${tScale})`;
|
const {
|
||||||
|
connectionLineType,
|
||||||
|
arrowHeadColor,
|
||||||
|
connectionLineStyle,
|
||||||
|
connectionLineComponent,
|
||||||
|
onlyRenderVisibleElements,
|
||||||
|
} = props;
|
||||||
|
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
|
||||||
const renderConnectionLine = connectionNodeId && connectionHandleType;
|
const renderConnectionLine = connectionNodeId && connectionHandleType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg width={width} height={height} className="react-flow__edges">
|
<svg width={width} height={height} className="react-flow__edges">
|
||||||
<MarkerDefinitions color={arrowHeadColor} />
|
<MarkerDefinitions color={arrowHeadColor} />
|
||||||
<g transform={transformStyle}>
|
<g transform={transformStyle}>
|
||||||
{edges.map((edge: Edge) => renderEdge(edge, props, nodes, selectedElements, elementsSelectable))}
|
{edges.map((edge: Edge) =>
|
||||||
|
renderEdge(
|
||||||
|
edge,
|
||||||
|
props,
|
||||||
|
nodes,
|
||||||
|
selectedElements,
|
||||||
|
elementsSelectable,
|
||||||
|
transform,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onlyRenderVisibleElements
|
||||||
|
)
|
||||||
|
)}
|
||||||
{renderConnectionLine && (
|
{renderConnectionLine && (
|
||||||
<ConnectionLine
|
<ConnectionLine
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -257,7 +177,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
|
|||||||
connectionHandleType={connectionHandleType!}
|
connectionHandleType={connectionHandleType!}
|
||||||
connectionPositionX={connectionPosition.x}
|
connectionPositionX={connectionPosition.x}
|
||||||
connectionPositionY={connectionPosition.y}
|
connectionPositionY={connectionPosition.y}
|
||||||
transform={[tX, tY, tScale]}
|
transform={transform}
|
||||||
connectionLineStyle={connectionLineStyle}
|
connectionLineStyle={connectionLineStyle}
|
||||||
connectionLineType={connectionLineType}
|
connectionLineType={connectionLineType}
|
||||||
isConnectable={nodesConnectable}
|
isConnectable={nodesConnectable}
|
||||||
|
|||||||
@@ -2,8 +2,19 @@ import { ComponentType } from 'react';
|
|||||||
|
|
||||||
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
|
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
|
||||||
import wrapEdge from '../../components/Edges/wrapEdge';
|
import wrapEdge from '../../components/Edges/wrapEdge';
|
||||||
|
import { rectToBox } from '../../utils/graph';
|
||||||
|
|
||||||
import { EdgeTypesType, EdgeProps } from '../../types';
|
import {
|
||||||
|
EdgeTypesType,
|
||||||
|
EdgeProps,
|
||||||
|
Position,
|
||||||
|
Node,
|
||||||
|
XYPosition,
|
||||||
|
ElementId,
|
||||||
|
HandleElement,
|
||||||
|
Transform,
|
||||||
|
Edge,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
|
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
|
||||||
const standardTypes: EdgeTypesType = {
|
const standardTypes: EdgeTypesType = {
|
||||||
@@ -27,3 +38,133 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
|
|||||||
...specialTypes,
|
...specialTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
|
||||||
|
const x = (handle?.x || 0) + node.__rf.position.x;
|
||||||
|
const y = (handle?.y || 0) + node.__rf.position.y;
|
||||||
|
const width = handle?.width || node.__rf.width;
|
||||||
|
const height = handle?.height || node.__rf.height;
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case Position.Top:
|
||||||
|
return {
|
||||||
|
x: x + width / 2,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
case Position.Right:
|
||||||
|
return {
|
||||||
|
x: x + width,
|
||||||
|
y: y + height / 2,
|
||||||
|
};
|
||||||
|
case Position.Bottom:
|
||||||
|
return {
|
||||||
|
x: x + width / 2,
|
||||||
|
y: y + height,
|
||||||
|
};
|
||||||
|
case Position.Left:
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y: y + height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null {
|
||||||
|
if (!bounds) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is no handleId when there are no multiple handles/ handles with ids
|
||||||
|
// so we just pick the first one
|
||||||
|
let handle = null;
|
||||||
|
if (bounds.length === 1 || !handleId) {
|
||||||
|
handle = bounds[0];
|
||||||
|
} else if (handleId) {
|
||||||
|
handle = bounds.find((d) => d.id === handleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof handle === 'undefined' ? null : handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgePositions {
|
||||||
|
sourceX: number;
|
||||||
|
sourceY: number;
|
||||||
|
targetX: number;
|
||||||
|
targetY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEdgePositions = (
|
||||||
|
sourceNode: Node,
|
||||||
|
sourceHandle: HandleElement | unknown,
|
||||||
|
sourcePosition: Position,
|
||||||
|
targetNode: Node,
|
||||||
|
targetHandle: HandleElement | unknown,
|
||||||
|
targetPosition: Position
|
||||||
|
): EdgePositions => {
|
||||||
|
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
|
||||||
|
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceX: sourceHandlePos.x,
|
||||||
|
sourceY: sourceHandlePos.y,
|
||||||
|
targetX: targetHandlePos.x,
|
||||||
|
targetY: targetHandlePos.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IsEdgeVisibleParams {
|
||||||
|
sourcePos: XYPosition;
|
||||||
|
targetPos: XYPosition;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
transform: Transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }: IsEdgeVisibleParams): boolean {
|
||||||
|
const edgeBox = {
|
||||||
|
x: Math.min(sourcePos.x, targetPos.x),
|
||||||
|
y: Math.min(sourcePos.y, targetPos.y),
|
||||||
|
x2: Math.max(sourcePos.x, targetPos.x),
|
||||||
|
y2: Math.max(sourcePos.y, targetPos.y),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (edgeBox.x === edgeBox.x2) {
|
||||||
|
edgeBox.x2 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edgeBox.y === edgeBox.y2) {
|
||||||
|
edgeBox.y2 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewBox = rectToBox({
|
||||||
|
x: (0 - transform[0]) / transform[2],
|
||||||
|
y: (0 - transform[1]) / transform[2],
|
||||||
|
width: width / transform[2],
|
||||||
|
height: height / transform[2],
|
||||||
|
});
|
||||||
|
|
||||||
|
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x));
|
||||||
|
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y));
|
||||||
|
const overlappingArea = Math.ceil(xOverlap * yOverlap);
|
||||||
|
|
||||||
|
return overlappingArea > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceTargetNode = {
|
||||||
|
sourceNode: Node | null;
|
||||||
|
targetNode: Node | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
|
||||||
|
return nodes.reduce(
|
||||||
|
(res, node) => {
|
||||||
|
if (node.id === edge.source) {
|
||||||
|
res.sourceNode = node;
|
||||||
|
} else if (node.id === edge.target) {
|
||||||
|
res.targetNode = node;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
{ sourceNode: null, targetNode: null } as SourceTargetNode
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ interface FlowRendererProps
|
|||||||
| 'snapGrid'
|
| 'snapGrid'
|
||||||
| 'connectionLineType'
|
| 'connectionLineType'
|
||||||
| 'arrowHeadColor'
|
| 'arrowHeadColor'
|
||||||
| 'onlyRenderVisibleNodes'
|
| 'onlyRenderVisibleElements'
|
||||||
|
| 'selectNodesOnDrag'
|
||||||
> {
|
> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ const FlowRenderer = ({
|
|||||||
zoomOnScroll,
|
zoomOnScroll,
|
||||||
panOnScroll,
|
panOnScroll,
|
||||||
panOnScrollSpeed,
|
panOnScrollSpeed,
|
||||||
|
panOnScrollMode,
|
||||||
zoomOnDoubleClick,
|
zoomOnDoubleClick,
|
||||||
paneMoveable,
|
paneMoveable,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
@@ -91,6 +93,7 @@ const FlowRenderer = ({
|
|||||||
zoomOnScroll={zoomOnScroll}
|
zoomOnScroll={zoomOnScroll}
|
||||||
panOnScroll={panOnScroll}
|
panOnScroll={panOnScroll}
|
||||||
panOnScrollSpeed={panOnScrollSpeed}
|
panOnScrollSpeed={panOnScrollSpeed}
|
||||||
|
panOnScrollMode={panOnScrollMode}
|
||||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||||
paneMoveable={paneMoveable}
|
paneMoveable={paneMoveable}
|
||||||
defaultPosition={defaultPosition}
|
defaultPosition={defaultPosition}
|
||||||
|
|||||||
@@ -1,80 +1,30 @@
|
|||||||
import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react';
|
import React, { useEffect, useRef, memo } from 'react';
|
||||||
|
|
||||||
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
|
import { useStoreActions, useStore } from '../../store/hooks';
|
||||||
import FlowRenderer from '../FlowRenderer';
|
import FlowRenderer from '../FlowRenderer';
|
||||||
import NodeRenderer from '../NodeRenderer';
|
import NodeRenderer from '../NodeRenderer';
|
||||||
import EdgeRenderer from '../EdgeRenderer';
|
import EdgeRenderer from '../EdgeRenderer';
|
||||||
import useElementUpdater from '../../hooks/useElementUpdater';
|
import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph';
|
||||||
import { onLoadProject, onLoadGetElements } from '../../utils/graph';
|
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
|
||||||
import {
|
|
||||||
Elements,
|
|
||||||
NodeTypesType,
|
|
||||||
EdgeTypesType,
|
|
||||||
OnLoadFunc,
|
|
||||||
Node,
|
|
||||||
Edge,
|
|
||||||
Connection,
|
|
||||||
ConnectionLineType,
|
|
||||||
ConnectionLineComponent,
|
|
||||||
FlowTransform,
|
|
||||||
OnConnectStartFunc,
|
|
||||||
OnConnectStopFunc,
|
|
||||||
OnConnectEndFunc,
|
|
||||||
TranslateExtent,
|
|
||||||
} from '../../types';
|
|
||||||
|
|
||||||
export interface GraphViewProps {
|
import { ReactFlowProps } from '../ReactFlow';
|
||||||
elements: Elements;
|
|
||||||
onElementClick?: (event: MouseEvent, element: Node | Edge) => void;
|
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
|
||||||
onElementsRemove?: (elements: Elements) => void;
|
|
||||||
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
|
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'elements'> {
|
||||||
onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
|
|
||||||
onNodeMouseLeave?: (event: MouseEvent, node: Node) => void;
|
|
||||||
onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
|
|
||||||
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
|
|
||||||
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
|
|
||||||
onConnect?: (connection: Connection | Edge) => void;
|
|
||||||
onConnectStart?: OnConnectStartFunc;
|
|
||||||
onConnectStop?: OnConnectStopFunc;
|
|
||||||
onConnectEnd?: OnConnectEndFunc;
|
|
||||||
onLoad?: OnLoadFunc;
|
|
||||||
onMove?: (flowTransform?: FlowTransform) => void;
|
|
||||||
onMoveStart?: (flowTransform?: FlowTransform) => void;
|
|
||||||
onMoveEnd?: (flowTransform?: FlowTransform) => void;
|
|
||||||
onPaneScroll?: (event?: WheelEvent) => void;
|
|
||||||
onPaneClick?: (event: MouseEvent) => void;
|
|
||||||
onPaneContextMenu?: (event: MouseEvent) => void;
|
|
||||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
|
|
||||||
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
|
|
||||||
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
|
|
||||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
|
||||||
selectionKeyCode: number;
|
|
||||||
nodeTypes: NodeTypesType;
|
nodeTypes: NodeTypesType;
|
||||||
edgeTypes: EdgeTypesType;
|
edgeTypes: EdgeTypesType;
|
||||||
|
selectionKeyCode: KeyCode;
|
||||||
|
deleteKeyCode: KeyCode;
|
||||||
|
multiSelectionKeyCode: KeyCode;
|
||||||
connectionLineType: ConnectionLineType;
|
connectionLineType: ConnectionLineType;
|
||||||
connectionLineStyle?: CSSProperties;
|
|
||||||
connectionLineComponent?: ConnectionLineComponent;
|
|
||||||
deleteKeyCode: number;
|
|
||||||
multiSelectionKeyCode: number;
|
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean;
|
onlyRenderVisibleElements: boolean;
|
||||||
nodesDraggable?: boolean;
|
|
||||||
nodesConnectable?: boolean;
|
|
||||||
elementsSelectable?: boolean;
|
|
||||||
selectNodesOnDrag?: boolean;
|
|
||||||
minZoom?: number;
|
|
||||||
maxZoom?: number;
|
|
||||||
defaultZoom: number;
|
defaultZoom: number;
|
||||||
defaultPosition: [number, number];
|
defaultPosition: [number, number];
|
||||||
translateExtent?: TranslateExtent;
|
|
||||||
arrowHeadColor: string;
|
arrowHeadColor: string;
|
||||||
markerEndId?: string;
|
selectNodesOnDrag: boolean;
|
||||||
zoomOnScroll?: boolean;
|
|
||||||
panOnScroll?: boolean;
|
|
||||||
panOnScrollSpeed?: number;
|
|
||||||
zoomOnDoubleClick?: boolean;
|
|
||||||
paneMoveable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphView = ({
|
const GraphView = ({
|
||||||
@@ -102,18 +52,17 @@ const GraphView = ({
|
|||||||
multiSelectionKeyCode,
|
multiSelectionKeyCode,
|
||||||
onElementsRemove,
|
onElementsRemove,
|
||||||
deleteKeyCode,
|
deleteKeyCode,
|
||||||
elements,
|
|
||||||
onConnect,
|
onConnect,
|
||||||
onConnectStart,
|
onConnectStart,
|
||||||
onConnectStop,
|
onConnectStop,
|
||||||
onConnectEnd,
|
onConnectEnd,
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
snapGrid,
|
snapGrid,
|
||||||
onlyRenderVisibleNodes,
|
onlyRenderVisibleElements,
|
||||||
nodesDraggable,
|
nodesDraggable,
|
||||||
nodesConnectable,
|
nodesConnectable,
|
||||||
elementsSelectable,
|
elementsSelectable,
|
||||||
selectNodesOnDrag = true,
|
selectNodesOnDrag,
|
||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
defaultZoom,
|
defaultZoom,
|
||||||
@@ -124,6 +73,7 @@ const GraphView = ({
|
|||||||
zoomOnScroll,
|
zoomOnScroll,
|
||||||
panOnScroll,
|
panOnScroll,
|
||||||
panOnScrollSpeed,
|
panOnScrollSpeed,
|
||||||
|
panOnScrollMode,
|
||||||
zoomOnDoubleClick,
|
zoomOnDoubleClick,
|
||||||
paneMoveable,
|
paneMoveable,
|
||||||
onPaneClick,
|
onPaneClick,
|
||||||
@@ -131,7 +81,6 @@ const GraphView = ({
|
|||||||
onPaneContextMenu,
|
onPaneContextMenu,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
const isInitialised = useRef<boolean>(false);
|
const isInitialised = useRef<boolean>(false);
|
||||||
const d3Initialised = useStoreState((state) => state.d3Initialised);
|
|
||||||
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
||||||
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
|
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
|
||||||
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
|
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
|
||||||
@@ -141,35 +90,30 @@ const GraphView = ({
|
|||||||
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
|
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
|
||||||
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
|
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
|
||||||
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
|
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
|
||||||
const setInitTransform = useStoreActions((actions) => actions.setInitTransform);
|
|
||||||
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
|
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
|
||||||
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
|
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
|
||||||
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
|
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
|
||||||
const fitView = useStoreActions((actions) => actions.fitView);
|
|
||||||
const zoom = useStoreActions((actions) => actions.zoom);
|
|
||||||
const zoomTo = useStoreActions((actions) => actions.zoomTo);
|
|
||||||
const currentStore = useStore();
|
const currentStore = useStore();
|
||||||
|
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
|
||||||
useElementUpdater(elements);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialised.current && d3Initialised) {
|
if (!isInitialised.current && initialized) {
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
onLoad({
|
onLoad({
|
||||||
fitView: (params = { padding: 0.1 }) => fitView(params),
|
fitView: (params = { padding: 0.1 }) => fitView(params),
|
||||||
zoomIn: () => zoom(0.2),
|
zoomIn,
|
||||||
zoomOut: () => zoom(-0.2),
|
zoomOut,
|
||||||
zoomTo: (zoomLevel) => zoomTo(zoomLevel),
|
zoomTo,
|
||||||
|
setTransform: transform,
|
||||||
project: onLoadProject(currentStore),
|
project: onLoadProject(currentStore),
|
||||||
getElements: onLoadGetElements(currentStore),
|
getElements: onLoadGetElements(currentStore),
|
||||||
setTransform: (transform: FlowTransform) =>
|
toObject: onLoadToObject(currentStore),
|
||||||
setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialised.current = true;
|
isInitialised.current = true;
|
||||||
}
|
}
|
||||||
}, [d3Initialised, onLoad]);
|
}, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onConnect) {
|
if (onConnect) {
|
||||||
@@ -260,6 +204,7 @@ const GraphView = ({
|
|||||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||||
panOnScroll={panOnScroll}
|
panOnScroll={panOnScroll}
|
||||||
panOnScrollSpeed={panOnScrollSpeed}
|
panOnScrollSpeed={panOnScrollSpeed}
|
||||||
|
panOnScrollMode={panOnScrollMode}
|
||||||
paneMoveable={paneMoveable}
|
paneMoveable={paneMoveable}
|
||||||
defaultPosition={defaultPosition}
|
defaultPosition={defaultPosition}
|
||||||
defaultZoom={defaultZoom}
|
defaultZoom={defaultZoom}
|
||||||
@@ -278,10 +223,10 @@ const GraphView = ({
|
|||||||
onNodeContextMenu={onNodeContextMenu}
|
onNodeContextMenu={onNodeContextMenu}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
onNodeDragStart={onNodeDragStart}
|
onNodeDragStart={onNodeDragStart}
|
||||||
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
|
|
||||||
selectNodesOnDrag={selectNodesOnDrag}
|
selectNodesOnDrag={selectNodesOnDrag}
|
||||||
snapToGrid={snapToGrid}
|
snapToGrid={snapToGrid}
|
||||||
snapGrid={snapGrid}
|
snapGrid={snapGrid}
|
||||||
|
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||||
/>
|
/>
|
||||||
<EdgeRenderer
|
<EdgeRenderer
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
@@ -291,6 +236,7 @@ const GraphView = ({
|
|||||||
arrowHeadColor={arrowHeadColor}
|
arrowHeadColor={arrowHeadColor}
|
||||||
markerEndId={markerEndId}
|
markerEndId={markerEndId}
|
||||||
connectionLineComponent={connectionLineComponent}
|
connectionLineComponent={connectionLineComponent}
|
||||||
|
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||||
/>
|
/>
|
||||||
</FlowRenderer>
|
</FlowRenderer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { memo, ComponentType, MouseEvent } from 'react';
|
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
|
||||||
|
|
||||||
import { useStoreState } from '../../store/hooks';
|
|
||||||
import { getNodesInside } from '../../utils/graph';
|
import { getNodesInside } from '../../utils/graph';
|
||||||
import { Node, Transform, NodeTypesType, WrapNodeProps, Elements, Edge } from '../../types';
|
import { useStoreState } from '../../store/hooks';
|
||||||
|
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
|
||||||
|
|
||||||
interface NodeRendererProps {
|
interface NodeRendererProps {
|
||||||
nodeTypes: NodeTypesType;
|
nodeTypes: NodeTypesType;
|
||||||
@@ -14,87 +14,76 @@ interface NodeRendererProps {
|
|||||||
onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
|
onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
|
||||||
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
|
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
|
||||||
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
|
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
|
||||||
onlyRenderVisibleNodes: boolean;
|
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
}
|
onlyRenderVisibleElements: boolean;
|
||||||
|
|
||||||
function renderNode(
|
|
||||||
node: Node,
|
|
||||||
props: NodeRendererProps,
|
|
||||||
transform: Transform,
|
|
||||||
selectedElements: Elements | null,
|
|
||||||
nodesDraggable: boolean,
|
|
||||||
nodesConnectable: boolean,
|
|
||||||
elementsSelectable: boolean
|
|
||||||
) {
|
|
||||||
const nodeType = node.type || 'default';
|
|
||||||
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
|
||||||
if (!props.nodeTypes[nodeType]) {
|
|
||||||
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false;
|
|
||||||
|
|
||||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
|
||||||
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
|
|
||||||
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
|
|
||||||
const isInitialized = node.__rf.width !== null && node.__rf.height !== null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeComponent
|
|
||||||
key={node.id}
|
|
||||||
id={node.id}
|
|
||||||
type={nodeType}
|
|
||||||
data={node.data}
|
|
||||||
xPos={node.__rf.position.x}
|
|
||||||
yPos={node.__rf.position.y}
|
|
||||||
isDragging={node.__rf.isDragging}
|
|
||||||
onClick={props.onElementClick}
|
|
||||||
onMouseEnter={props.onNodeMouseEnter}
|
|
||||||
onMouseMove={props.onNodeMouseMove}
|
|
||||||
onMouseLeave={props.onNodeMouseLeave}
|
|
||||||
onContextMenu={props.onNodeContextMenu}
|
|
||||||
onNodeDragStart={props.onNodeDragStart}
|
|
||||||
onNodeDragStop={props.onNodeDragStop}
|
|
||||||
transform={transform}
|
|
||||||
selected={isSelected}
|
|
||||||
style={node.style}
|
|
||||||
className={node.className}
|
|
||||||
isDraggable={isDraggable}
|
|
||||||
isSelectable={isSelectable}
|
|
||||||
isConnectable={isConnectable}
|
|
||||||
sourcePosition={node.sourcePosition}
|
|
||||||
targetPosition={node.targetPosition}
|
|
||||||
selectNodesOnDrag={props.selectNodesOnDrag}
|
|
||||||
isHidden={node.isHidden}
|
|
||||||
isInitialized={isInitialized}
|
|
||||||
snapGrid={props.snapGrid}
|
|
||||||
snapToGrid={props.snapToGrid}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeRenderer = (props: NodeRendererProps) => {
|
const NodeRenderer = (props: NodeRendererProps) => {
|
||||||
const nodes = useStoreState((s) => s.nodes);
|
const transform = useStoreState((state) => state.transform);
|
||||||
const transform = useStoreState((s) => s.transform);
|
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
|
||||||
const viewportBox = useStoreState((s) => s.viewportBox);
|
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
|
||||||
const nodesDraggable = useStoreState((s) => s.nodesDraggable);
|
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
||||||
const nodesConnectable = useStoreState((s) => s.nodesConnectable);
|
const viewportBox = useStoreState((state) => state.viewportBox);
|
||||||
const elementsSelectable = useStoreState((s) => s.elementsSelectable);
|
const nodes = useStoreState((state) => state.nodes);
|
||||||
|
|
||||||
const transformStyle = {
|
const visibleNodes = props.onlyRenderVisibleElements ? getNodesInside(nodes, viewportBox, transform, true) : nodes;
|
||||||
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNodes = props.onlyRenderVisibleNodes ? getNodesInside(nodes, viewportBox, transform, true) : nodes;
|
const transformStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
|
||||||
|
}),
|
||||||
|
[transform[0], transform[1], transform[2]]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__nodes" style={transformStyle}>
|
<div className="react-flow__nodes" style={transformStyle}>
|
||||||
{renderNodes.map((node) =>
|
{visibleNodes.map((node) => {
|
||||||
renderNode(node, props, transform, selectedElements, nodesDraggable, nodesConnectable, elementsSelectable)
|
const nodeType = node.type || 'default';
|
||||||
)}
|
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
||||||
|
|
||||||
|
if (!props.nodeTypes[nodeType]) {
|
||||||
|
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
||||||
|
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
|
||||||
|
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeComponent
|
||||||
|
key={node.id}
|
||||||
|
id={node.id}
|
||||||
|
className={node.className}
|
||||||
|
style={node.style}
|
||||||
|
type={nodeType}
|
||||||
|
data={node.data}
|
||||||
|
sourcePosition={node.sourcePosition}
|
||||||
|
targetPosition={node.targetPosition}
|
||||||
|
isHidden={node.isHidden}
|
||||||
|
xPos={node.__rf.position.x}
|
||||||
|
yPos={node.__rf.position.y}
|
||||||
|
isDragging={node.__rf.isDragging}
|
||||||
|
isInitialized={node.__rf.width !== null && node.__rf.height !== null}
|
||||||
|
snapGrid={props.snapGrid}
|
||||||
|
snapToGrid={props.snapToGrid}
|
||||||
|
selectNodesOnDrag={props.selectNodesOnDrag}
|
||||||
|
onClick={props.onElementClick}
|
||||||
|
onMouseEnter={props.onNodeMouseEnter}
|
||||||
|
onMouseMove={props.onNodeMouseMove}
|
||||||
|
onMouseLeave={props.onNodeMouseLeave}
|
||||||
|
onContextMenu={props.onNodeContextMenu}
|
||||||
|
onNodeDragStart={props.onNodeDragStart}
|
||||||
|
onNodeDragStop={props.onNodeDragStop}
|
||||||
|
scale={transform[2]}
|
||||||
|
selected={selectedElements?.some(({ id }) => id === node.id) || false}
|
||||||
|
isDraggable={isDraggable}
|
||||||
|
isSelectable={isSelectable}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { StoreProvider, useStore } from 'easy-peasy';
|
import { StoreProvider } from 'easy-peasy';
|
||||||
|
|
||||||
import store, { StoreModel } from '../../store';
|
import store from '../../store';
|
||||||
|
import { useStore } from '../../store/hooks';
|
||||||
|
|
||||||
const Wrapper: FC = ({ children }) => {
|
const Wrapper: FC = ({ children }) => {
|
||||||
const easyPeasyStore = useStore<StoreModel>();
|
const easyPeasyStore = useStore();
|
||||||
const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion;
|
const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion;
|
||||||
|
|
||||||
if (isWrapepdWithReactFlowProvider) {
|
if (isWrapepdWithReactFlowProvider) {
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import React, { useMemo, CSSProperties, HTMLAttributes, MouseEvent, WheelEvent } from 'react';
|
import React, { useMemo, CSSProperties, HTMLAttributes, MouseEvent, WheelEvent } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;
|
|
||||||
|
|
||||||
if (nodeEnv !== 'production') {
|
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
|
||||||
whyDidYouRender(React);
|
|
||||||
}
|
|
||||||
|
|
||||||
import GraphView from '../GraphView';
|
import GraphView from '../GraphView';
|
||||||
|
import ElementUpdater from '../../components/ElementUpdater';
|
||||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
import DefaultNode from '../../components/Nodes/DefaultNode';
|
||||||
import InputNode from '../../components/Nodes/InputNode';
|
import InputNode from '../../components/Nodes/InputNode';
|
||||||
import OutputNode from '../../components/Nodes/OutputNode';
|
import OutputNode from '../../components/Nodes/OutputNode';
|
||||||
@@ -32,6 +26,8 @@ import {
|
|||||||
OnConnectStopFunc,
|
OnConnectStopFunc,
|
||||||
OnConnectEndFunc,
|
OnConnectEndFunc,
|
||||||
TranslateExtent,
|
TranslateExtent,
|
||||||
|
KeyCode,
|
||||||
|
PanOnScrollMode,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import '../../style.css';
|
import '../../style.css';
|
||||||
@@ -80,12 +76,12 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
|||||||
connectionLineType?: ConnectionLineType;
|
connectionLineType?: ConnectionLineType;
|
||||||
connectionLineStyle?: CSSProperties;
|
connectionLineStyle?: CSSProperties;
|
||||||
connectionLineComponent?: ConnectionLineComponent;
|
connectionLineComponent?: ConnectionLineComponent;
|
||||||
deleteKeyCode?: number;
|
deleteKeyCode?: KeyCode;
|
||||||
selectionKeyCode?: number;
|
selectionKeyCode?: KeyCode;
|
||||||
multiSelectionKeyCode?: number;
|
multiSelectionKeyCode?: KeyCode;
|
||||||
snapToGrid?: boolean;
|
snapToGrid?: boolean;
|
||||||
snapGrid?: [number, number];
|
snapGrid?: [number, number];
|
||||||
onlyRenderVisibleNodes?: boolean;
|
onlyRenderVisibleElements?: boolean;
|
||||||
nodesDraggable?: boolean;
|
nodesDraggable?: boolean;
|
||||||
nodesConnectable?: boolean;
|
nodesConnectable?: boolean;
|
||||||
elementsSelectable?: boolean;
|
elementsSelectable?: boolean;
|
||||||
@@ -101,6 +97,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
|||||||
zoomOnScroll?: boolean;
|
zoomOnScroll?: boolean;
|
||||||
panOnScroll?: boolean;
|
panOnScroll?: boolean;
|
||||||
panOnScrollSpeed?: number;
|
panOnScrollSpeed?: number;
|
||||||
|
panOnScrollMode?: PanOnScrollMode;
|
||||||
zoomOnDoubleClick?: boolean;
|
zoomOnDoubleClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +130,12 @@ const ReactFlow = ({
|
|||||||
connectionLineType = ConnectionLineType.Bezier,
|
connectionLineType = ConnectionLineType.Bezier,
|
||||||
connectionLineStyle,
|
connectionLineStyle,
|
||||||
connectionLineComponent,
|
connectionLineComponent,
|
||||||
deleteKeyCode = 8,
|
deleteKeyCode = 'Backspace',
|
||||||
selectionKeyCode = 16,
|
selectionKeyCode = 'Shift',
|
||||||
multiSelectionKeyCode = 91,
|
multiSelectionKeyCode = 'Meta',
|
||||||
snapToGrid = false,
|
snapToGrid = false,
|
||||||
snapGrid = [15, 15],
|
snapGrid = [15, 15],
|
||||||
onlyRenderVisibleNodes = true,
|
onlyRenderVisibleElements = true,
|
||||||
selectNodesOnDrag = true,
|
selectNodesOnDrag = true,
|
||||||
nodesDraggable,
|
nodesDraggable,
|
||||||
nodesConnectable,
|
nodesConnectable,
|
||||||
@@ -153,6 +150,7 @@ const ReactFlow = ({
|
|||||||
zoomOnScroll = true,
|
zoomOnScroll = true,
|
||||||
panOnScroll = false,
|
panOnScroll = false,
|
||||||
panOnScrollSpeed = 0.5,
|
panOnScrollSpeed = 0.5,
|
||||||
|
panOnScrollMode = PanOnScrollMode.Free,
|
||||||
zoomOnDoubleClick = true,
|
zoomOnDoubleClick = true,
|
||||||
paneMoveable = true,
|
paneMoveable = true,
|
||||||
onPaneClick,
|
onPaneClick,
|
||||||
@@ -189,14 +187,13 @@ const ReactFlow = ({
|
|||||||
onElementsRemove={onElementsRemove}
|
onElementsRemove={onElementsRemove}
|
||||||
deleteKeyCode={deleteKeyCode}
|
deleteKeyCode={deleteKeyCode}
|
||||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||||
elements={elements}
|
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onConnectStart={onConnectStart}
|
onConnectStart={onConnectStart}
|
||||||
onConnectStop={onConnectStop}
|
onConnectStop={onConnectStop}
|
||||||
onConnectEnd={onConnectEnd}
|
onConnectEnd={onConnectEnd}
|
||||||
snapToGrid={snapToGrid}
|
snapToGrid={snapToGrid}
|
||||||
snapGrid={snapGrid}
|
snapGrid={snapGrid}
|
||||||
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
|
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||||
nodesDraggable={nodesDraggable}
|
nodesDraggable={nodesDraggable}
|
||||||
nodesConnectable={nodesConnectable}
|
nodesConnectable={nodesConnectable}
|
||||||
elementsSelectable={elementsSelectable}
|
elementsSelectable={elementsSelectable}
|
||||||
@@ -212,6 +209,7 @@ const ReactFlow = ({
|
|||||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||||
panOnScroll={panOnScroll}
|
panOnScroll={panOnScroll}
|
||||||
panOnScrollSpeed={panOnScrollSpeed}
|
panOnScrollSpeed={panOnScrollSpeed}
|
||||||
|
panOnScrollMode={panOnScrollMode}
|
||||||
paneMoveable={paneMoveable}
|
paneMoveable={paneMoveable}
|
||||||
onPaneClick={onPaneClick}
|
onPaneClick={onPaneClick}
|
||||||
onPaneScroll={onPaneScroll}
|
onPaneScroll={onPaneScroll}
|
||||||
@@ -221,6 +219,7 @@ const ReactFlow = ({
|
|||||||
onSelectionDragStop={onSelectionDragStop}
|
onSelectionDragStop={onSelectionDragStop}
|
||||||
onSelectionContextMenu={onSelectionContextMenu}
|
onSelectionContextMenu={onSelectionContextMenu}
|
||||||
/>
|
/>
|
||||||
|
<ElementUpdater elements={elements} />
|
||||||
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import React, { useEffect, useRef, ReactNode } from 'react';
|
import React, { useEffect, useRef, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { zoom, zoomIdentity } from 'd3-zoom';
|
||||||
|
import { select } from 'd3-selection';
|
||||||
|
import { clamp } from '../../utils';
|
||||||
|
|
||||||
import useResizeHandler from '../../hooks/useResizeHandler';
|
import useResizeHandler from '../../hooks/useResizeHandler';
|
||||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
|
||||||
import { FlowTransform, TranslateExtent } from '../../types';
|
import { FlowTransform, TranslateExtent, PanOnScrollMode } from '../../types';
|
||||||
|
|
||||||
interface ZoomPaneProps {
|
interface ZoomPaneProps {
|
||||||
selectionKeyPressed: boolean;
|
selectionKeyPressed: boolean;
|
||||||
@@ -10,6 +14,7 @@ interface ZoomPaneProps {
|
|||||||
zoomOnScroll?: boolean;
|
zoomOnScroll?: boolean;
|
||||||
panOnScroll?: boolean;
|
panOnScroll?: boolean;
|
||||||
panOnScrollSpeed?: number;
|
panOnScrollSpeed?: number;
|
||||||
|
panOnScrollMode?: PanOnScrollMode;
|
||||||
zoomOnDoubleClick?: boolean;
|
zoomOnDoubleClick?: boolean;
|
||||||
paneMoveable?: boolean;
|
paneMoveable?: boolean;
|
||||||
defaultPosition?: [number, number];
|
defaultPosition?: [number, number];
|
||||||
@@ -39,6 +44,7 @@ const ZoomPane = ({
|
|||||||
zoomOnScroll = true,
|
zoomOnScroll = true,
|
||||||
panOnScroll = false,
|
panOnScroll = false,
|
||||||
panOnScrollSpeed = 0.5,
|
panOnScrollSpeed = 0.5,
|
||||||
|
panOnScrollMode = PanOnScrollMode.Free,
|
||||||
zoomOnDoubleClick = true,
|
zoomOnDoubleClick = true,
|
||||||
selectionKeyPressed,
|
selectionKeyPressed,
|
||||||
elementsSelectable,
|
elementsSelectable,
|
||||||
@@ -51,18 +57,37 @@ const ZoomPane = ({
|
|||||||
const zoomPane = useRef<HTMLDivElement>(null);
|
const zoomPane = useRef<HTMLDivElement>(null);
|
||||||
const prevTransform = useRef<FlowTransform>({ x: 0, y: 0, zoom: 0 });
|
const prevTransform = useRef<FlowTransform>({ x: 0, y: 0, zoom: 0 });
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
const d3Zoom = useStoreState((s) => s.d3Zoom);
|
const d3Zoom = useStoreState((s) => s.d3Zoom);
|
||||||
const d3Selection = useStoreState((s) => s.d3Selection);
|
const d3Selection = useStoreState((s) => s.d3Selection);
|
||||||
const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler);
|
const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler);
|
||||||
|
|
||||||
const initD3 = useStoreActions((actions) => actions.initD3);
|
const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom);
|
||||||
const updateTransform = useStoreActions((actions) => actions.updateTransform);
|
const updateTransform = useStoreActions((actions) => actions.updateTransform);
|
||||||
|
|
||||||
useResizeHandler(zoomPane);
|
useResizeHandler(zoomPane);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoomPane.current) {
|
if (zoomPane.current) {
|
||||||
initD3({ zoomPane: zoomPane.current, defaultPosition, defaultZoom, translateExtent });
|
const state = store.getState();
|
||||||
|
const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
|
||||||
|
const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
|
||||||
|
const selection = select(zoomPane.current as Element).call(d3ZoomInstance);
|
||||||
|
|
||||||
|
const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
|
||||||
|
const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
|
||||||
|
const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
|
||||||
|
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
|
||||||
|
|
||||||
|
d3ZoomInstance.transform(selection, updatedTransform);
|
||||||
|
|
||||||
|
initD3Zoom({
|
||||||
|
d3Zoom: d3ZoomInstance,
|
||||||
|
d3Selection: selection,
|
||||||
|
d3ZoomHandler: selection.on('wheel.zoom'),
|
||||||
|
// we need to pass transform because zoom handler is not registered when we set the initial transform
|
||||||
|
transform: [clampedX, clampedY, clampedZoom],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -78,13 +103,13 @@ const ZoomPane = ({
|
|||||||
// increase scroll speed in firefox
|
// increase scroll speed in firefox
|
||||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
||||||
const deltaX = event.deltaX * deltaNormalize;
|
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
||||||
const deltaY = event.deltaY * deltaNormalize;
|
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
||||||
|
|
||||||
d3Zoom.translateBy(
|
d3Zoom.translateBy(
|
||||||
d3Selection,
|
d3Selection,
|
||||||
(deltaX / currentZoom) * panOnScrollSpeed,
|
-(deltaX / currentZoom) * panOnScrollSpeed,
|
||||||
(deltaY / currentZoom) * panOnScrollSpeed
|
-(deltaY / currentZoom) * panOnScrollSpeed
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.on('wheel.zoom', null);
|
.on('wheel.zoom', null);
|
||||||
@@ -92,7 +117,7 @@ const ZoomPane = ({
|
|||||||
d3Selection.on('wheel', null).on('wheel.zoom', d3ZoomHandler);
|
d3Selection.on('wheel', null).on('wheel.zoom', d3ZoomHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [panOnScroll, d3Selection, d3Zoom, d3ZoomHandler]);
|
}, [panOnScroll, panOnScrollMode, d3Selection, d3Zoom, d3ZoomHandler]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (d3Zoom) {
|
if (d3Zoom) {
|
||||||
@@ -100,7 +125,7 @@ const ZoomPane = ({
|
|||||||
d3Zoom.on('zoom', null);
|
d3Zoom.on('zoom', null);
|
||||||
} else {
|
} else {
|
||||||
d3Zoom.on('zoom', (event: any) => {
|
d3Zoom.on('zoom', (event: any) => {
|
||||||
updateTransform(event.transform);
|
updateTransform([event.transform.x, event.transform.y, event.transform.k]);
|
||||||
|
|
||||||
if (onMove) {
|
if (onMove) {
|
||||||
const flowTransform = eventToFlowTransform(event.transform);
|
const flowTransform = eventToFlowTransform(event.transform);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useStoreState, useStoreActions } from '../store/hooks';
|
import { useStore, useStoreActions } from '../store/hooks';
|
||||||
import useKeyPress from './useKeyPress';
|
import useKeyPress from './useKeyPress';
|
||||||
import { isNode, getConnectedEdges } from '../utils/graph';
|
import { isNode, getConnectedEdges } from '../utils/graph';
|
||||||
import { Elements } from '../types';
|
import { Elements, KeyCode } from '../types';
|
||||||
|
|
||||||
interface HookParams {
|
interface HookParams {
|
||||||
deleteKeyCode: number;
|
deleteKeyCode: KeyCode;
|
||||||
multiSelectionKeyCode: number;
|
multiSelectionKeyCode: KeyCode;
|
||||||
onElementsRemove?: (elements: Elements) => void;
|
onElementsRemove?: (elements: Elements) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
||||||
const selectedElements = useStoreState((state) => state.selectedElements);
|
const store = useStore();
|
||||||
const edges = useStoreState((state) => state.edges);
|
|
||||||
|
|
||||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
|
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
|
||||||
@@ -23,6 +22,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
|
|||||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const { edges, selectedElements } = store.getState();
|
||||||
if (onElementsRemove && deleteKeyPressed && selectedElements) {
|
if (onElementsRemove && deleteKeyPressed && selectedElements) {
|
||||||
let elementsToRemove = selectedElements;
|
let elementsToRemove = selectedElements;
|
||||||
|
|
||||||
|
|||||||
+21
-14
@@ -1,31 +1,38 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { isInputDOMNode } from '../utils';
|
import { isInputDOMNode } from '../utils';
|
||||||
|
import { KeyCode } from '../types';
|
||||||
|
|
||||||
export default (keyCode: number): boolean => {
|
export default (keyCode: KeyCode): boolean => {
|
||||||
const [keyPressed, setKeyPressed] = useState(false);
|
const [keyPressed, setKeyPressed] = useState(false);
|
||||||
|
|
||||||
function downHandler(event: KeyboardEvent) {
|
|
||||||
if (event.keyCode === keyCode && !isInputDOMNode(event)) {
|
|
||||||
setKeyPressed(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const upHandler = (event: KeyboardEvent) => {
|
|
||||||
if (event.keyCode === keyCode && !isInputDOMNode(event)) {
|
|
||||||
setKeyPressed(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const downHandler = (event: KeyboardEvent) => {
|
||||||
|
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setKeyPressed(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upHandler = (event: KeyboardEvent) => {
|
||||||
|
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
|
||||||
|
setKeyPressed(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHandler = () => setKeyPressed(false);
|
||||||
|
|
||||||
window.addEventListener('keydown', downHandler);
|
window.addEventListener('keydown', downHandler);
|
||||||
window.addEventListener('keyup', upHandler);
|
window.addEventListener('keyup', upHandler);
|
||||||
|
window.addEventListener('blur', resetHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', downHandler);
|
window.removeEventListener('keydown', downHandler);
|
||||||
window.removeEventListener('keyup', upHandler);
|
window.removeEventListener('keyup', upHandler);
|
||||||
|
window.removeEventListener('blur', resetHandler);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [keyCode, setKeyPressed]);
|
||||||
|
|
||||||
return keyPressed;
|
return keyPressed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, MutableRefObject } from 'react';
|
import { useEffect, MutableRefObject } from 'react';
|
||||||
import { ResizeObserver } from 'resize-observer';
|
|
||||||
import { useStoreActions } from '../store/hooks';
|
import { useStoreActions } from '../store/hooks';
|
||||||
|
|
||||||
import { getDimensions } from '../utils';
|
import { getDimensions } from '../utils';
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { zoomIdentity } from 'd3-zoom';
|
||||||
|
|
||||||
|
import { useStoreState, useStore } from '../store/hooks';
|
||||||
|
import { clamp } from '../utils';
|
||||||
|
import { getRectOfNodes } from '../utils/graph';
|
||||||
|
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions } from '../types';
|
||||||
|
|
||||||
|
const initialZoomPanHelper: ZoomPanHelperFunctions = {
|
||||||
|
zoomIn: () => {},
|
||||||
|
zoomOut: () => {},
|
||||||
|
zoomTo: (_: number) => {},
|
||||||
|
transform: (_: FlowTransform) => {},
|
||||||
|
fitView: (_: FitViewParams = { padding: 0.1 }) => {},
|
||||||
|
initialized: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePanZoomHelper = (): ZoomPanHelperFunctions => {
|
||||||
|
const store = useStore();
|
||||||
|
const d3Zoom = useStoreState((s) => s.d3Zoom);
|
||||||
|
const d3Selection = useStoreState((s) => s.d3Selection);
|
||||||
|
|
||||||
|
const zoomPanHelperFunctions = useMemo<ZoomPanHelperFunctions>(() => {
|
||||||
|
if (d3Selection && d3Zoom) {
|
||||||
|
return {
|
||||||
|
zoomIn: () => d3Zoom.scaleBy(d3Selection, 1.2),
|
||||||
|
zoomOut: () => d3Zoom.scaleBy(d3Selection, 1 / 1.2),
|
||||||
|
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
||||||
|
transform: (transform: FlowTransform) => {
|
||||||
|
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom);
|
||||||
|
|
||||||
|
d3Zoom.transform(d3Selection, nextTransform);
|
||||||
|
},
|
||||||
|
fitView: (options: FitViewParams = { padding: 0.1 }) => {
|
||||||
|
const { nodes, width, height, minZoom, maxZoom } = store.getState();
|
||||||
|
|
||||||
|
if (!nodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = getRectOfNodes(nodes);
|
||||||
|
const xZoom = width / (bounds.width * (1 + options.padding));
|
||||||
|
const yZoom = height / (bounds.height * (1 + options.padding));
|
||||||
|
const zoom = Math.min(xZoom, yZoom);
|
||||||
|
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||||
|
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||||
|
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||||
|
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||||
|
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||||
|
const transform = zoomIdentity.translate(x, y).scale(clampedZoom);
|
||||||
|
|
||||||
|
d3Zoom.transform(d3Selection, transform);
|
||||||
|
},
|
||||||
|
initialized: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialZoomPanHelper;
|
||||||
|
}, [d3Zoom, d3Selection]);
|
||||||
|
|
||||||
|
return zoomPanHelperFunctions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePanZoomHelper;
|
||||||
@@ -9,6 +9,7 @@ export { getSmoothStepPath } from './components/Edges/SmoothStepEdge';
|
|||||||
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
|
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
|
||||||
|
|
||||||
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph';
|
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph';
|
||||||
|
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
|
||||||
|
|
||||||
export * from './additional-components';
|
export * from './additional-components';
|
||||||
export * from './store/hooks';
|
export * from './store/hooks';
|
||||||
|
|||||||
+23
-129
@@ -1,13 +1,11 @@
|
|||||||
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
|
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
|
||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
||||||
import { zoom, zoomIdentity } from 'd3-zoom';
|
|
||||||
import { select } from 'd3-selection';
|
|
||||||
|
|
||||||
import { getDimensions, clamp } from '../utils';
|
import { getDimensions } from '../utils';
|
||||||
|
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
|
||||||
import { getHandleBounds } from '../components/Nodes/utils';
|
import { getHandleBounds } from '../components/Nodes/utils';
|
||||||
|
|
||||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
|
|
||||||
import {
|
import {
|
||||||
ElementId,
|
ElementId,
|
||||||
Elements,
|
Elements,
|
||||||
@@ -26,43 +24,35 @@ import {
|
|||||||
SetConnectionId,
|
SetConnectionId,
|
||||||
NodePosUpdate,
|
NodePosUpdate,
|
||||||
NodeDiffUpdate,
|
NodeDiffUpdate,
|
||||||
FitViewParams,
|
|
||||||
TranslateExtent,
|
TranslateExtent,
|
||||||
SnapGrid,
|
SnapGrid,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
type TransformXYK = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
k: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NodeDimensionUpdate = {
|
type NodeDimensionUpdate = {
|
||||||
id: ElementId;
|
id: ElementId;
|
||||||
nodeElement: HTMLDivElement;
|
nodeElement: HTMLDivElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitD3 = {
|
type InitD3Zoom = {
|
||||||
zoomPane: Element;
|
d3Zoom: ZoomBehavior<Element, unknown>;
|
||||||
defaultPosition: [number, number];
|
d3Selection: D3Selection<Element, unknown, null, undefined>;
|
||||||
defaultZoom: number;
|
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||||
translateExtent?: TranslateExtent;
|
transform: Transform;
|
||||||
};
|
};
|
||||||
export interface StoreModel {
|
export interface StoreModel {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
viewportBox: Computed<StoreModel, Rect>;
|
|
||||||
transform: Transform;
|
transform: Transform;
|
||||||
elements: Elements;
|
elements: Elements;
|
||||||
nodes: Computed<StoreModel, Node[]>;
|
nodes: Computed<StoreModel, Node[]>;
|
||||||
edges: Computed<StoreModel, Edge[]>;
|
edges: Computed<StoreModel, Edge[]>;
|
||||||
selectedElements: Elements | null;
|
selectedElements: Elements | null;
|
||||||
selectedNodesBbox: Rect;
|
selectedNodesBbox: Rect;
|
||||||
|
viewportBox: Computed<StoreModel, Rect>;
|
||||||
|
|
||||||
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
||||||
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||||
d3Initialised: boolean;
|
|
||||||
minZoom: number;
|
minZoom: number;
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
translateExtent: TranslateExtent;
|
translateExtent: TranslateExtent;
|
||||||
@@ -113,13 +103,11 @@ export interface StoreModel {
|
|||||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
||||||
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
||||||
|
|
||||||
updateTransform: Action<StoreModel, TransformXYK>;
|
updateTransform: Action<StoreModel, Transform>;
|
||||||
|
|
||||||
setInitTransform: Action<StoreModel, TransformXYK>;
|
|
||||||
|
|
||||||
updateSize: Action<StoreModel, Dimensions>;
|
updateSize: Action<StoreModel, Dimensions>;
|
||||||
|
|
||||||
initD3: Action<StoreModel, InitD3>;
|
initD3Zoom: Action<StoreModel, InitD3Zoom>;
|
||||||
|
|
||||||
setMinZoom: Action<StoreModel, number>;
|
setMinZoom: Action<StoreModel, number>;
|
||||||
setMaxZoom: Action<StoreModel, number>;
|
setMaxZoom: Action<StoreModel, number>;
|
||||||
@@ -142,29 +130,22 @@ export interface StoreModel {
|
|||||||
updateUserSelection: Action<StoreModel, XYPosition>;
|
updateUserSelection: Action<StoreModel, XYPosition>;
|
||||||
unsetUserSelection: Action<StoreModel>;
|
unsetUserSelection: Action<StoreModel>;
|
||||||
|
|
||||||
fitView: Action<StoreModel, FitViewParams>;
|
|
||||||
zoomTo: Action<StoreModel, number>;
|
|
||||||
zoom: Thunk<StoreModel, number, any, StoreModel>;
|
|
||||||
zoomIn: Thunk<StoreModel>;
|
|
||||||
zoomOut: Thunk<StoreModel>;
|
|
||||||
|
|
||||||
setMultiSelectionActive: Action<StoreModel, boolean>;
|
setMultiSelectionActive: Action<StoreModel, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storeModel: StoreModel = {
|
export const storeModel: StoreModel = {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
|
||||||
transform: [0, 0, 1],
|
transform: [0, 0, 1],
|
||||||
elements: [],
|
elements: [],
|
||||||
nodes: computed((state) => state.elements.filter((el) => isNode(el)) as Node[]),
|
nodes: computed((state) => state.elements.filter(isNode)),
|
||||||
edges: computed((state) => state.elements.filter((el) => isEdge(el)) as Edge[]),
|
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
||||||
|
edges: computed((state) => state.elements.filter(isEdge)),
|
||||||
selectedElements: null,
|
selectedElements: null,
|
||||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||||
|
|
||||||
d3Zoom: null,
|
d3Zoom: null,
|
||||||
d3Selection: null,
|
d3Selection: null,
|
||||||
d3Initialised: false,
|
|
||||||
d3ZoomHandler: undefined,
|
d3ZoomHandler: undefined,
|
||||||
minZoom: 0.5,
|
minZoom: 0.5,
|
||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
@@ -373,21 +354,9 @@ export const storeModel: StoreModel = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
updateTransform: action((state, transform) => {
|
updateTransform: action((state, transform) => {
|
||||||
state.transform[0] = transform.x;
|
state.transform[0] = transform[0];
|
||||||
state.transform[1] = transform.y;
|
state.transform[1] = transform[1];
|
||||||
state.transform[2] = transform.k;
|
state.transform[2] = transform[2];
|
||||||
}),
|
|
||||||
|
|
||||||
setInitTransform: action((state, transform) => {
|
|
||||||
state.transform[0] = transform.x;
|
|
||||||
state.transform[1] = transform.y;
|
|
||||||
state.transform[2] = transform.k;
|
|
||||||
|
|
||||||
if (state.d3Selection) {
|
|
||||||
const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k);
|
|
||||||
// we need to sync the d3 zoom transform with the updated transform
|
|
||||||
state.d3Selection.property('__zoom', updatedTransform);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSize: action((state, size) => {
|
updateSize: action((state, size) => {
|
||||||
@@ -397,27 +366,14 @@ export const storeModel: StoreModel = {
|
|||||||
state.height = size.height || 500;
|
state.height = size.height || 500;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => {
|
initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler, transform }) => {
|
||||||
const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
|
state.d3Zoom = d3Zoom;
|
||||||
const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
|
state.d3Selection = d3Selection;
|
||||||
const selection = select(zoomPane).call(d3ZoomInstance);
|
state.d3ZoomHandler = d3ZoomHandler;
|
||||||
|
|
||||||
const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
|
state.transform[0] = transform[0];
|
||||||
const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
|
state.transform[1] = transform[1];
|
||||||
const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
|
state.transform[2] = transform[2];
|
||||||
|
|
||||||
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
|
|
||||||
selection.property('__zoom', updatedTransform);
|
|
||||||
|
|
||||||
const defaultHandler = selection.on('wheel.zoom');
|
|
||||||
|
|
||||||
state.transform[0] = clampedX;
|
|
||||||
state.transform[1] = clampedY;
|
|
||||||
state.transform[2] = clampedZoom;
|
|
||||||
state.d3Zoom = d3ZoomInstance;
|
|
||||||
state.d3Selection = selection;
|
|
||||||
state.d3ZoomHandler = defaultHandler;
|
|
||||||
state.d3Initialised = true;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setMinZoom: action((state, minZoom) => {
|
setMinZoom: action((state, minZoom) => {
|
||||||
@@ -481,68 +437,6 @@ export const storeModel: StoreModel = {
|
|||||||
state.elementsSelectable = elementsSelectable;
|
state.elementsSelectable = elementsSelectable;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
fitView: action((state, payload = { padding: 0.1 }) => {
|
|
||||||
const { padding } = payload;
|
|
||||||
const { nodes, width, height, d3Selection, minZoom, maxZoom } = state;
|
|
||||||
|
|
||||||
if (!d3Selection || !nodes.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = getRectOfNodes(nodes);
|
|
||||||
const xZoom = width / (bounds.width * (1 + padding));
|
|
||||||
const yZoom = height / (bounds.height * (1 + padding));
|
|
||||||
const zoom = Math.min(xZoom, yZoom);
|
|
||||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
|
||||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
||||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
||||||
const transform = [width / 2 - boundsCenterX * clampedZoom, height / 2 - boundsCenterY * clampedZoom];
|
|
||||||
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(clampedZoom);
|
|
||||||
|
|
||||||
// we need to sync the d3 zoom transform with the fitted transform
|
|
||||||
d3Selection.property('__zoom', fittedTransform);
|
|
||||||
|
|
||||||
state.transform[0] = fittedTransform.x;
|
|
||||||
state.transform[1] = fittedTransform.y;
|
|
||||||
state.transform[2] = fittedTransform.k;
|
|
||||||
}),
|
|
||||||
|
|
||||||
zoomTo: action((state, zoomLevel) => {
|
|
||||||
const { d3Selection, transform, minZoom, maxZoom } = state;
|
|
||||||
const nextZoom = clamp(zoomLevel, minZoom, maxZoom);
|
|
||||||
|
|
||||||
if (d3Selection) {
|
|
||||||
// we want to zoom in and out to the center of the zoom pane
|
|
||||||
const center = [state.width / 2, state.height / 2];
|
|
||||||
const centerInverted = [(center[0] - transform[0]) / transform[2], (center[1] - transform[1]) / transform[2]];
|
|
||||||
const x = center[0] - centerInverted[0] * nextZoom;
|
|
||||||
const y = center[1] - centerInverted[1] * nextZoom;
|
|
||||||
const zoomedTransform = zoomIdentity.translate(x, y).scale(nextZoom);
|
|
||||||
|
|
||||||
// we need to sync the d3 zoom transform with the zoomed transform
|
|
||||||
d3Selection.property('__zoom', zoomedTransform);
|
|
||||||
|
|
||||||
state.transform[0] = zoomedTransform.x;
|
|
||||||
state.transform[1] = zoomedTransform.y;
|
|
||||||
state.transform[2] = zoomedTransform.k;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
zoom: thunk((actions, amount, helpers) => {
|
|
||||||
const { transform } = helpers.getState();
|
|
||||||
const nextZoom = transform[2] + amount;
|
|
||||||
|
|
||||||
actions.zoomTo(nextZoom);
|
|
||||||
}),
|
|
||||||
|
|
||||||
zoomIn: thunk((actions) => {
|
|
||||||
actions.zoom(0.2);
|
|
||||||
}),
|
|
||||||
|
|
||||||
zoomOut: thunk((actions) => {
|
|
||||||
actions.zoom(-0.2);
|
|
||||||
}),
|
|
||||||
|
|
||||||
setMultiSelectionActive: action((state, isActive) => {
|
setMultiSelectionActive: action((state, isActive) => {
|
||||||
state.multiSelectionActive = isActive;
|
state.multiSelectionActive = isActive;
|
||||||
}),
|
}),
|
||||||
|
|||||||
+27
-1
@@ -201,7 +201,7 @@ export interface WrapNodeProps {
|
|||||||
type: string;
|
type: string;
|
||||||
data: any;
|
data: any;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
transform: Transform;
|
scale: number;
|
||||||
xPos: number;
|
xPos: number;
|
||||||
yPos: number;
|
yPos: number;
|
||||||
isSelectable: boolean;
|
isSelectable: boolean;
|
||||||
@@ -229,8 +229,16 @@ export interface WrapNodeProps {
|
|||||||
export type FitViewParams = {
|
export type FitViewParams = {
|
||||||
padding: number;
|
padding: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FlowExportObject = {
|
||||||
|
elements: Elements;
|
||||||
|
position: [number, number];
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type FitViewFunc = (fitViewOptions?: FitViewParams) => void;
|
export type FitViewFunc = (fitViewOptions?: FitViewParams) => void;
|
||||||
export type ProjectFunc = (position: XYPosition) => XYPosition;
|
export type ProjectFunc = (position: XYPosition) => XYPosition;
|
||||||
|
export type ToObjectFunc = () => FlowExportObject;
|
||||||
|
|
||||||
export type OnLoadParams = {
|
export type OnLoadParams = {
|
||||||
zoomIn: () => void;
|
zoomIn: () => void;
|
||||||
@@ -240,6 +248,7 @@ export type OnLoadParams = {
|
|||||||
project: ProjectFunc;
|
project: ProjectFunc;
|
||||||
getElements: () => Elements;
|
getElements: () => Elements;
|
||||||
setTransform: (transform: FlowTransform) => void;
|
setTransform: (transform: FlowTransform) => void;
|
||||||
|
toObject: ToObjectFunc;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OnLoadFunc = (params: OnLoadParams) => void;
|
export type OnLoadFunc = (params: OnLoadParams) => void;
|
||||||
@@ -320,3 +329,20 @@ export type FlowTransform = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TranslateExtent = [[number, number], [number, number]];
|
export type TranslateExtent = [[number, number], [number, number]];
|
||||||
|
|
||||||
|
export type KeyCode = number | string;
|
||||||
|
|
||||||
|
export enum PanOnScrollMode {
|
||||||
|
Free = 'free',
|
||||||
|
Vertical = 'vertical',
|
||||||
|
Horizontal = 'horizontal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZoomPanHelperFunctions {
|
||||||
|
zoomIn: () => void;
|
||||||
|
zoomOut: () => void;
|
||||||
|
zoomTo: (zoomLevel: number) => void;
|
||||||
|
transform: (transform: FlowTransform) => void;
|
||||||
|
fitView: (params?: FitViewParams) => void;
|
||||||
|
initialized: boolean;
|
||||||
|
}
|
||||||
|
|||||||
+29
-34
@@ -1,7 +1,17 @@
|
|||||||
import { Store } from 'easy-peasy';
|
import { Store } from 'easy-peasy';
|
||||||
|
import { StoreModel } from '../store';
|
||||||
import store, { StoreModel } from '../store';
|
import {
|
||||||
import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types';
|
ElementId,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Elements,
|
||||||
|
Transform,
|
||||||
|
XYPosition,
|
||||||
|
Rect,
|
||||||
|
Box,
|
||||||
|
Connection,
|
||||||
|
FlowExportObject,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
||||||
'id' in element && 'source' in element && 'target' in element;
|
'id' in element && 'source' in element && 'target' in element;
|
||||||
@@ -106,12 +116,6 @@ export const onLoadProject = (currentStore: Store<StoreModel>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const project = (position: XYPosition): XYPosition => {
|
|
||||||
const { transform, snapToGrid, snapGrid } = store.getState();
|
|
||||||
|
|
||||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseElement = (element: Node | Edge): Node | Edge => {
|
export const parseElement = (element: Node | Edge): Node | Edge => {
|
||||||
if (!element.id) {
|
if (!element.id) {
|
||||||
throw new Error('All nodes and edges need to have an id.');
|
throw new Error('All nodes and edges need to have an id.');
|
||||||
@@ -150,14 +154,14 @@ const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
|||||||
y2: Math.max(box1.y2, box2.y2),
|
y2: Math.max(box1.y2, box2.y2),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
x2: x + width,
|
x2: x + width,
|
||||||
y2: y + height,
|
y2: y + height,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: x2 - x,
|
width: x2 - x,
|
||||||
@@ -202,7 +206,7 @@ export const getNodesInside = (
|
|||||||
const overlappingArea = Math.ceil(xOverlap * yOverlap);
|
const overlappingArea = Math.ceil(xOverlap * yOverlap);
|
||||||
|
|
||||||
if (width === null || height === null || isDragging) {
|
if (width === null || height === null || isDragging) {
|
||||||
// at the beginnning all nodes have width & height === 0
|
// nodes are initialized with width and height = null
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,33 +221,18 @@ export const getNodesInside = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
|
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
|
||||||
const nodeIds = nodes.map((n) => n.id);
|
const nodeIds = nodes.map((node) => node.id);
|
||||||
|
|
||||||
return edges.filter((e) => {
|
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
|
||||||
const sourceId = e.source.split('__')[0];
|
|
||||||
const targetId = e.target.split('__')[0];
|
|
||||||
|
|
||||||
return nodeIds.includes(sourceId) || nodeIds.includes(targetId);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fitView = (params: FitViewParams = { padding: 0.1 }): void => {
|
|
||||||
store.getActions().fitView(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoom = (amount: number): void => {
|
|
||||||
store.getActions().zoom(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const zoomIn = (): void => zoom(0.2);
|
|
||||||
|
|
||||||
export const zoomOut = (): void => zoom(-0.2);
|
|
||||||
|
|
||||||
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
|
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
|
||||||
return [
|
return [
|
||||||
...nodes.map((node) => {
|
...nodes.map((node) => {
|
||||||
const n = { ...node };
|
const n = { ...node };
|
||||||
|
|
||||||
|
n.position = n.__rf.position;
|
||||||
|
|
||||||
delete n.__rf;
|
delete n.__rf;
|
||||||
return n;
|
return n;
|
||||||
}),
|
}),
|
||||||
@@ -259,8 +248,14 @@ export const onLoadGetElements = (currentStore: Store<StoreModel>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElements = (): Elements => {
|
export const onLoadToObject = (currentStore: Store<StoreModel>) => {
|
||||||
const { nodes = [], edges = [] } = store.getState();
|
return (): FlowExportObject => {
|
||||||
|
const { nodes = [], edges = [], transform } = currentStore.getState();
|
||||||
|
|
||||||
return parseElements(nodes, edges);
|
return {
|
||||||
|
elements: parseElements(nodes, edges),
|
||||||
|
position: [transform[0], transform[1]],
|
||||||
|
zoom: transform[2],
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user