@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import ReactFlow, { addEdge } from 'react-flow-renderer';
|
import ReactFlow, { addEdge, isEdge } from 'react-flow-renderer';
|
||||||
|
|
||||||
const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
|
const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
|
||||||
|
|
||||||
@@ -23,19 +23,20 @@ const initialElements = [
|
|||||||
{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true },
|
{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HorizontalFlow = () => {
|
const NodeTypeChangeFlow = () => {
|
||||||
const [elements, setElements] = useState(initialElements);
|
const [elements, setElements] = useState(initialElements);
|
||||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||||
const changeType = () => {
|
const changeType = () => {
|
||||||
setElements((elms) =>
|
setElements((elms) =>
|
||||||
elms.map((el) => {
|
elms.map((el) => {
|
||||||
if (el.type === 'input') {
|
if (isEdge(el) || el.type === 'input') {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.type = el.type === 'default' ? 'output' : 'default';
|
return {
|
||||||
|
...el,
|
||||||
return { ...el };
|
type: el.type === 'default' ? 'output' : 'default',
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -49,4 +50,4 @@ const HorizontalFlow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HorizontalFlow;
|
export default NodeTypeChangeFlow;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useStoreState, useStoreActions } from 'react-flow-renderer';
|
import { useStoreState, useStoreActions, isNode } from 'react-flow-renderer';
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const nodes = useStoreState((store) => store.nodes);
|
const nodes = useStoreState((store) => store.nodes);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const onLoad = (reactFlowInstance) => {
|
|||||||
console.log(reactFlowInstance.getElements());
|
console.log(reactFlowInstance.getElements());
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialElements = getElements(10, 10);
|
const initialElements = getElements(30, 30);
|
||||||
|
|
||||||
const StressFlow = () => {
|
const StressFlow = () => {
|
||||||
const [elements, setElements] = useState(initialElements);
|
const [elements, setElements] = useState(initialElements);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import ReactFlow, {
|
||||||
|
removeElements,
|
||||||
|
addEdge,
|
||||||
|
Background,
|
||||||
|
MiniMap,
|
||||||
|
useZoomPanHelper,
|
||||||
|
ReactFlowProvider,
|
||||||
|
} from 'react-flow-renderer';
|
||||||
|
|
||||||
|
const initialElements = [
|
||||||
|
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||||
|
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||||
|
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||||
|
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||||
|
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||||
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let id = 5;
|
||||||
|
const getId = () => `${id++}`;
|
||||||
|
|
||||||
|
const UseZoomPanHelperFlow = () => {
|
||||||
|
const [elements, setElements] = useState(initialElements);
|
||||||
|
const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
|
||||||
|
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||||
|
const { project } = useZoomPanHelper();
|
||||||
|
|
||||||
|
const onPaneClick = useCallback(
|
||||||
|
(evt) => {
|
||||||
|
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
|
||||||
|
|
||||||
|
setElements((els) =>
|
||||||
|
els.concat({
|
||||||
|
id: getId(),
|
||||||
|
position: projectedPosition,
|
||||||
|
data: {
|
||||||
|
label: `${projectedPosition.x}-${projectedPosition.y}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[project]
|
||||||
|
);
|
||||||
|
console.log(elements);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect} onPaneClick={onPaneClick}>
|
||||||
|
<Background />
|
||||||
|
<MiniMap />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<UseZoomPanHelperFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
@@ -21,6 +21,7 @@ import SaveRestore from './SaveRestore';
|
|||||||
import DragNDrop from './DragNDrop';
|
import DragNDrop from './DragNDrop';
|
||||||
import Layout from './Layouting';
|
import Layout from './Layouting';
|
||||||
import SwitchFlows from './Switch';
|
import SwitchFlows from './Switch';
|
||||||
|
import UseZoomPanHelper from './UseZoomPanHelper';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -101,6 +102,10 @@ const routes = [
|
|||||||
path: '/switch',
|
path: '/switch',
|
||||||
component: SwitchFlows,
|
component: SwitchFlows,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/usezoompanhelper',
|
||||||
|
component: UseZoomPanHelper,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const Header = withRouter(({ history, location }) => {
|
const Header = withRouter(({ history, location }) => {
|
||||||
|
|||||||
Generated
+12450
-1549
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-flow-renderer",
|
"name": "react-flow-renderer",
|
||||||
"version": "8.8.0",
|
"version": "9.0.0-next.1",
|
||||||
"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",
|
||||||
@@ -33,9 +33,10 @@
|
|||||||
"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",
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"react-draggable": "^4.4.3"
|
"react-draggable": "^4.4.3",
|
||||||
|
"react-redux": "^7.2.2",
|
||||||
|
"redux": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.16",
|
"@babel/core": "^7.12.16",
|
||||||
@@ -50,8 +51,8 @@
|
|||||||
"@svgr/rollup": "^5.5.0",
|
"@svgr/rollup": "^5.5.0",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/d3": "^6.3.0",
|
"@types/d3": "^6.3.0",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react-redux": "^7.1.15",
|
||||||
"@types/react-dom": "^17.0.1",
|
"@types/redux": "^3.6.0",
|
||||||
"@types/resize-observer-browser": "^0.1.5",
|
"@types/resize-observer-browser": "^0.1.5",
|
||||||
"autoprefixer": "^10.2.4",
|
"autoprefixer": "^10.2.4",
|
||||||
"babel-preset-react-app": "^10.0.0",
|
"babel-preset-react-app": "^10.0.0",
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import React, { useMemo, FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { StoreProvider, createStore } from 'easy-peasy';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { storeModel } from '../../store';
|
import { initialState } from '../../store';
|
||||||
|
import configureStore from '../../store/configure-store';
|
||||||
|
|
||||||
const ReactFlowProvider: FC = ({ children }) => {
|
const ReactFlowProvider: FC = ({ children }) => {
|
||||||
const store = useMemo(() => {
|
const store = useMemo(() => {
|
||||||
return createStore(storeModel);
|
return configureStore(initialState);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <StoreProvider store={store}>{children}</StoreProvider>;
|
return <Provider store={store}>{children}</Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactFlowProvider.displayName = 'ReactFlowProvider';
|
ReactFlowProvider.displayName = 'ReactFlowProvider';
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
type ValidConnectionFunc = (connection: Connection) => boolean;
|
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||||
export type SetSourceIdFunc = (params: SetConnectionId) => void;
|
export type SetSourceIdFunc = (params: SetConnectionId) => void;
|
||||||
|
|
||||||
|
export type SetPosition = (pos: XYPosition) => void;
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
elementBelow: Element | null;
|
elementBelow: Element | null;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
@@ -86,7 +88,7 @@ export function onMouseDown(
|
|||||||
handleId: ElementId | null,
|
handleId: ElementId | null,
|
||||||
nodeId: ElementId,
|
nodeId: ElementId,
|
||||||
setConnectionNodeId: SetSourceIdFunc,
|
setConnectionNodeId: SetSourceIdFunc,
|
||||||
setPosition: (pos: XYPosition) => void,
|
setPosition: SetPosition,
|
||||||
onConnect: OnConnectFunc,
|
onConnect: OnConnectFunc,
|
||||||
isTarget: boolean,
|
isTarget: boolean,
|
||||||
isValidConnection: ValidConnectionFunc,
|
isValidConnection: ValidConnectionFunc,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useStoreActions, useStoreState } from '../../store/hooks';
|
|||||||
import NodeIdContext from '../../contexts/NodeIdContext';
|
import NodeIdContext from '../../contexts/NodeIdContext';
|
||||||
import { HandleProps, Connection, ElementId, Position } from '../../types';
|
import { HandleProps, Connection, ElementId, Position } from '../../types';
|
||||||
|
|
||||||
import { onMouseDown } from './handler';
|
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
|
||||||
|
|
||||||
const alwaysValid = () => true;
|
const alwaysValid = () => true;
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ const Handle: FunctionComponent<HandleProps & Omit<HTMLAttributes<HTMLDivElement
|
|||||||
event,
|
event,
|
||||||
handleId,
|
handleId,
|
||||||
nodeId,
|
nodeId,
|
||||||
setConnectionNodeId,
|
(setConnectionNodeId as unknown) as SetSourceIdFunc,
|
||||||
setPosition,
|
(setPosition as unknown) as SetPosition,
|
||||||
onConnectExtended,
|
onConnectExtended,
|
||||||
isTarget,
|
isTarget,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodeElement.current && !isHidden) {
|
if (nodeElement.current && !isHidden) {
|
||||||
updateNodeDimensions({ id, nodeElement: nodeElement.current });
|
updateNodeDimensions([{ id, nodeElement: nodeElement.current }]);
|
||||||
}
|
}
|
||||||
}, [id, isHidden, sourcePosition, targetPosition]);
|
}, [id, isHidden, sourcePosition, targetPosition]);
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ const Edge = ({
|
|||||||
|
|
||||||
const EdgeRenderer = (props: EdgeRendererProps) => {
|
const EdgeRenderer = (props: EdgeRendererProps) => {
|
||||||
const transform = useStoreState((state) => state.transform);
|
const transform = useStoreState((state) => state.transform);
|
||||||
|
const nodes = useStoreState((state) => state.nodes);
|
||||||
const edges = useStoreState((state) => state.edges);
|
const edges = useStoreState((state) => state.edges);
|
||||||
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
|
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
|
||||||
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
|
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
|
||||||
@@ -179,7 +180,6 @@ 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);
|
|
||||||
|
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
|
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
|
||||||
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
|
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
|
||||||
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
||||||
const viewportBox = useStoreState((state) => state.viewportBox);
|
const width = useStoreState((state) => state.width);
|
||||||
|
const height = useStoreState((state) => state.height);
|
||||||
const nodes = useStoreState((state) => state.nodes);
|
const nodes = useStoreState((state) => state.nodes);
|
||||||
const batchUpdateNodeDimensions = useStoreActions((actions) => actions.batchUpdateNodeDimensions);
|
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
|
||||||
|
|
||||||
const visibleNodes = props.onlyRenderVisibleElements ? getNodesInside(nodes, viewportBox, transform, true) : nodes;
|
const visibleNodes = props.onlyRenderVisibleElements
|
||||||
|
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
|
||||||
|
: nodes;
|
||||||
|
|
||||||
const transformStyle = useMemo(
|
const transformStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -43,13 +46,13 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ResizeObserver((entries) => {
|
return new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||||
const updates = entries.map((entry) => ({
|
const updates = entries.map((entry: ResizeObserverEntry) => ({
|
||||||
id: entry.target.getAttribute('data-id') as string,
|
id: entry.target.getAttribute('data-id') as string,
|
||||||
nodeElement: entry.target as HTMLDivElement,
|
nodeElement: entry.target as HTMLDivElement,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
batchUpdateNodeDimensions({ updates });
|
updateNodeDimensions(updates);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, useContext } from 'react';
|
||||||
import { StoreProvider } from 'easy-peasy';
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
|
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import { useStore } from '../../store/hooks';
|
|
||||||
|
|
||||||
const Wrapper: FC = ({ children }) => {
|
const Wrapper: FC = ({ children }) => {
|
||||||
const easyPeasyStore = useStore();
|
const contextValue = useContext(ReactReduxContext);
|
||||||
const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion;
|
const isWrappedWithReactFlowProvider = contextValue?.store?.getState()?.reactFlowVersion;
|
||||||
|
|
||||||
if (isWrapepdWithReactFlowProvider) {
|
if (isWrappedWithReactFlowProvider) {
|
||||||
// we need to wrap it with a fragment because t's not allowed for children to be a ReactNode
|
// we need to wrap it with a fragment because t's not allowed for children to be a ReactNode
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <StoreProvider store={store}>{children}</StoreProvider>;
|
return <Provider store={store}>{children}</Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
Wrapper.displayName = 'ReactFlowWrapper';
|
Wrapper.displayName = 'ReactFlowWrapper';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { zoomIdentity } from 'd3-zoom';
|
|||||||
|
|
||||||
import { useStoreState, useStore } from '../store/hooks';
|
import { useStoreState, useStore } from '../store/hooks';
|
||||||
import { clamp } from '../utils';
|
import { clamp } from '../utils';
|
||||||
import { getRectOfNodes } from '../utils/graph';
|
import { getRectOfNodes, pointToRendererPoint } from '../utils/graph';
|
||||||
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, Transform } from '../types';
|
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, Transform, XYPosition } from '../types';
|
||||||
|
|
||||||
const DEFAULT_PADDING = 0.1;
|
const DEFAULT_PADDING = 0.1;
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ const initialZoomPanHelper: ZoomPanHelperFunctions = {
|
|||||||
fitView: (_: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {},
|
fitView: (_: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {},
|
||||||
setCenter: (_: number, __: number) => {},
|
setCenter: (_: number, __: number) => {},
|
||||||
fitBounds: (_: Rect) => {},
|
fitBounds: (_: Rect) => {},
|
||||||
|
project: (position: XYPosition) => position,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,6 +87,10 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
|
|||||||
|
|
||||||
d3Zoom.transform(d3Selection, transform);
|
d3Zoom.transform(d3Selection, transform);
|
||||||
},
|
},
|
||||||
|
project: (position: XYPosition) => {
|
||||||
|
const { transform, snapToGrid, snapGrid } = store.getState();
|
||||||
|
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
|
||||||
|
},
|
||||||
initialized: true,
|
initialized: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { createAction } from './utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Elements,
|
||||||
|
OnConnectEndFunc,
|
||||||
|
OnConnectFunc,
|
||||||
|
OnConnectStartFunc,
|
||||||
|
OnConnectStopFunc,
|
||||||
|
NodeDimensionUpdate,
|
||||||
|
NodePosUpdate,
|
||||||
|
NodeDiffUpdate,
|
||||||
|
XYPosition,
|
||||||
|
Transform,
|
||||||
|
Dimensions,
|
||||||
|
InitD3ZoomPayload,
|
||||||
|
TranslateExtent,
|
||||||
|
SetConnectionId,
|
||||||
|
SnapGrid,
|
||||||
|
ConnectionMode,
|
||||||
|
NodeExtent,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import * as constants from './contants';
|
||||||
|
|
||||||
|
export const setOnConnect = (onConnect: OnConnectFunc) =>
|
||||||
|
createAction(constants.SET_ON_CONNECT, {
|
||||||
|
onConnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setOnConnectStart = (onConnectStart: OnConnectStartFunc) =>
|
||||||
|
createAction(constants.SET_ON_CONNECT_START, {
|
||||||
|
onConnectStart,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setOnConnectStop = (onConnectStop: OnConnectStopFunc) =>
|
||||||
|
createAction(constants.SET_ON_CONNECT_STOP, {
|
||||||
|
onConnectStop,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) =>
|
||||||
|
createAction(constants.SET_ON_CONNECT_END, {
|
||||||
|
onConnectEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setElements = (elements: Elements) => createAction(constants.SET_ELEMENTS, elements);
|
||||||
|
|
||||||
|
export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) =>
|
||||||
|
createAction(constants.UPDATE_NODE_DIMENSIONS, updates);
|
||||||
|
|
||||||
|
export const updateNodePos = (payload: NodePosUpdate) => createAction(constants.UPDATE_NODE_POS, payload);
|
||||||
|
|
||||||
|
export const updateNodePosDiff = (payload: NodeDiffUpdate) => createAction(constants.UPDATE_NODE_POS_DIFF, payload);
|
||||||
|
|
||||||
|
export const setUserSelection = (mousePos: XYPosition) => createAction(constants.SET_USER_SELECTION, mousePos);
|
||||||
|
|
||||||
|
export const updateUserSelection = (mousePos: XYPosition) => createAction(constants.UPDATE_USER_SELECTION, mousePos);
|
||||||
|
|
||||||
|
export const unsetUserSelection = () => createAction(constants.UNSET_USER_SELECTION);
|
||||||
|
|
||||||
|
export const setSelection = (selectionActive: boolean) =>
|
||||||
|
createAction(constants.SET_SELECTION, {
|
||||||
|
selectionActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unsetNodesSelection = () =>
|
||||||
|
createAction(constants.UNSET_NODES_SELECTION, {
|
||||||
|
nodesSelectionActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resetSelectedElements = () =>
|
||||||
|
createAction(constants.RESET_SELECTED_ELEMENTS, {
|
||||||
|
selectedElements: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSelectedElements = (elements: Elements) => createAction(constants.SET_SELECTED_ELEMENTS, elements);
|
||||||
|
|
||||||
|
export const addSelectedElements = (elements: Elements) => createAction(constants.ADD_SELECTED_ELEMENTS, elements);
|
||||||
|
|
||||||
|
export const updateTransform = (transform: Transform) => createAction(constants.UPDATE_TRANSFORM, { transform });
|
||||||
|
|
||||||
|
export const updateSize = (size: Dimensions) =>
|
||||||
|
createAction(constants.UPDATE_SIZE, {
|
||||||
|
width: size.width || 500,
|
||||||
|
height: size.height || 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const initD3Zoom = (payload: InitD3ZoomPayload) => createAction(constants.INIT_D3ZOOM, payload);
|
||||||
|
|
||||||
|
export const setMinZoom = (minZoom: number) => createAction(constants.SET_MINZOOM, minZoom);
|
||||||
|
|
||||||
|
export const setMaxZoom = (maxZoom: number) => createAction(constants.SET_MAXZOOM, maxZoom);
|
||||||
|
|
||||||
|
export const setTranslateExtent = (translateExtent: TranslateExtent) =>
|
||||||
|
createAction(constants.SET_TRANSLATEEXTENT, translateExtent);
|
||||||
|
|
||||||
|
export const setConnectionPosition = (connectionPosition: XYPosition) =>
|
||||||
|
createAction(constants.SET_CONNECTION_POSITION, { connectionPosition });
|
||||||
|
|
||||||
|
export const setConnectionNodeId = (payload: SetConnectionId) => createAction(constants.SET_CONNECTION_NODEID, payload);
|
||||||
|
|
||||||
|
export const setSnapToGrid = (snapToGrid: boolean) => createAction(constants.SET_SNAPTOGRID, { snapToGrid });
|
||||||
|
|
||||||
|
export const setSnapGrid = (snapGrid: SnapGrid) => createAction(constants.SET_SNAPGRID, { snapGrid });
|
||||||
|
|
||||||
|
export const setInteractive = (isInteractive: boolean) =>
|
||||||
|
createAction(constants.SET_INTERACTIVE, {
|
||||||
|
nodesDraggable: isInteractive,
|
||||||
|
nodesConnectable: isInteractive,
|
||||||
|
elementsSelectable: isInteractive,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setNodesDraggable = (nodesDraggable: boolean) =>
|
||||||
|
createAction(constants.SET_NODES_DRAGGABLE, { nodesDraggable });
|
||||||
|
|
||||||
|
export const setNodesConnectable = (nodesConnectable: boolean) =>
|
||||||
|
createAction(constants.SET_NODES_CONNECTABLE, { nodesConnectable });
|
||||||
|
|
||||||
|
export const setElementsSelectable = (elementsSelectable: boolean) =>
|
||||||
|
createAction(constants.SET_ELEMENTS_SELECTABLE, { elementsSelectable });
|
||||||
|
|
||||||
|
export const setMultiSelectionActive = (multiSelectionActive: boolean) =>
|
||||||
|
createAction(constants.SET_MULTI_SELECTION_ACTIVE, { multiSelectionActive });
|
||||||
|
|
||||||
|
export const setConnectionMode = (connectionMode: ConnectionMode) =>
|
||||||
|
createAction(constants.SET_CONNECTION_MODE, { connectionMode });
|
||||||
|
|
||||||
|
export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent);
|
||||||
|
|
||||||
|
export type ReactFlowAction = ReturnType<
|
||||||
|
| typeof setOnConnect
|
||||||
|
| typeof setOnConnectStart
|
||||||
|
| typeof setOnConnectStop
|
||||||
|
| typeof setOnConnectEnd
|
||||||
|
| typeof setElements
|
||||||
|
| typeof updateNodeDimensions
|
||||||
|
| typeof updateNodePos
|
||||||
|
| typeof updateNodePosDiff
|
||||||
|
| typeof setUserSelection
|
||||||
|
| typeof updateUserSelection
|
||||||
|
| typeof unsetUserSelection
|
||||||
|
| typeof setSelection
|
||||||
|
| typeof unsetNodesSelection
|
||||||
|
| typeof resetSelectedElements
|
||||||
|
| typeof setSelectedElements
|
||||||
|
| typeof addSelectedElements
|
||||||
|
| typeof updateTransform
|
||||||
|
| typeof updateSize
|
||||||
|
| typeof initD3Zoom
|
||||||
|
| typeof setMinZoom
|
||||||
|
| typeof setMaxZoom
|
||||||
|
| typeof setTranslateExtent
|
||||||
|
| typeof setConnectionPosition
|
||||||
|
| typeof setConnectionNodeId
|
||||||
|
| typeof setSnapToGrid
|
||||||
|
| typeof setSnapGrid
|
||||||
|
| typeof setInteractive
|
||||||
|
| typeof setNodesDraggable
|
||||||
|
| typeof setNodesConnectable
|
||||||
|
| typeof setElementsSelectable
|
||||||
|
| typeof setMultiSelectionActive
|
||||||
|
| typeof setConnectionMode
|
||||||
|
| typeof setNodeExtent
|
||||||
|
>;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createStore, Store } from 'redux';
|
||||||
|
|
||||||
|
import { ReactFlowState } from '../types';
|
||||||
|
import { ReactFlowAction } from './actions';
|
||||||
|
import reactFlowReducer from './reducer';
|
||||||
|
|
||||||
|
export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> {
|
||||||
|
const store = createStore(reactFlowReducer, preloadedState);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const SET_ON_CONNECT = 'SET_ON_CONNECT';
|
||||||
|
export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START';
|
||||||
|
export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP';
|
||||||
|
export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END';
|
||||||
|
export const SET_ELEMENTS = 'SET_ELEMENTS';
|
||||||
|
export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS';
|
||||||
|
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
|
||||||
|
export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF';
|
||||||
|
export const SET_USER_SELECTION = 'SET_USER_SELECTION';
|
||||||
|
export const UPDATE_USER_SELECTION = 'UPDATE_USER_SELECTION';
|
||||||
|
export const UNSET_USER_SELECTION = 'UNSET_USER_SELECTION';
|
||||||
|
export const SET_SELECTION = 'SET_SELECTION';
|
||||||
|
export const UNSET_NODES_SELECTION = 'UNSET_NODES_SELECTION';
|
||||||
|
export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS';
|
||||||
|
export const RESET_SELECTED_ELEMENTS = 'RESET_SELECTED_ELEMENTS';
|
||||||
|
export const ADD_SELECTED_ELEMENTS = 'ADD_SELECTED_ELEMENTS';
|
||||||
|
export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
|
||||||
|
export const UPDATE_SIZE = 'UPDATE_SIZE';
|
||||||
|
export const INIT_D3ZOOM = 'INIT_D3ZOOM';
|
||||||
|
export const SET_MINZOOM = 'SET_MINZOOM';
|
||||||
|
export const SET_MAXZOOM = 'SET_MAXZOOM';
|
||||||
|
export const SET_TRANSLATEEXTENT = 'SET_TRANSLATEEXTENT';
|
||||||
|
export const SET_CONNECTION_POSITION = 'SET_CONNECTION_POSITION';
|
||||||
|
export const SET_CONNECTION_NODEID = 'SET_CONNECTION_NODEID';
|
||||||
|
export const SET_SNAPTOGRID = 'SET_SNAPTOGRID';
|
||||||
|
export const SET_SNAPGRID = 'SET_SNAPGRID';
|
||||||
|
export const SET_INTERACTIVE = 'SET_INTERACTIVE';
|
||||||
|
export const SET_NODES_DRAGGABLE = 'SET_NODES_DRAGGABLE';
|
||||||
|
export const SET_NODES_CONNECTABLE = 'SET_NODES_CONNECTABLE';
|
||||||
|
export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE';
|
||||||
|
export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE';
|
||||||
|
export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE';
|
||||||
|
export const SET_NODE_EXTENT = 'SET_NODE_EXTENT';
|
||||||
+45
-7
@@ -1,10 +1,48 @@
|
|||||||
import { createTypedHooks } from 'easy-peasy';
|
import { bindActionCreators, Store, ActionCreator, ActionCreatorsMapObject } from 'redux';
|
||||||
|
import {
|
||||||
|
useStore as useStoreRedux,
|
||||||
|
useSelector,
|
||||||
|
useDispatch as reduxUseDispatch,
|
||||||
|
TypedUseSelectorHook,
|
||||||
|
} from 'react-redux';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { StoreModel } from './index';
|
import { ReactFlowDispatch } from './index';
|
||||||
|
import * as actions from './actions';
|
||||||
|
import { ReactFlowAction } from './actions';
|
||||||
|
import { ReactFlowState } from '../types';
|
||||||
|
|
||||||
const typedHooks = createTypedHooks<StoreModel>();
|
export const useTypedSelector: TypedUseSelectorHook<ReactFlowState> = useSelector;
|
||||||
|
|
||||||
export const useStoreActions = typedHooks.useStoreActions;
|
export type ActionCreatorSelector<Action> = (acts: typeof actions) => ActionCreator<Action>;
|
||||||
export const useStoreDispatch = typedHooks.useStoreDispatch;
|
export type ActionMapObjectSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action>;
|
||||||
export const useStoreState = typedHooks.useStoreState;
|
export type ActionSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action> | ActionCreator<Action>;
|
||||||
export const useStore = typedHooks.useStore;
|
|
||||||
|
export function useStoreActions<Action extends ReactFlowAction>(
|
||||||
|
actionSelector: ActionCreatorSelector<Action>
|
||||||
|
): ActionCreator<Action>;
|
||||||
|
|
||||||
|
export function useStoreActions<Action extends ReactFlowAction>(
|
||||||
|
actionSelector: ActionMapObjectSelector<Action>
|
||||||
|
): ActionCreatorsMapObject<Action>;
|
||||||
|
|
||||||
|
export function useStoreActions<Action extends ReactFlowAction>(actionSelector: ActionSelector<Action>) {
|
||||||
|
const dispatch: ReactFlowDispatch = reduxUseDispatch();
|
||||||
|
const currAction = actionSelector(actions);
|
||||||
|
|
||||||
|
const action = useMemo(() => {
|
||||||
|
// this looks weird but required if both ActionSelector and ActionMapObjectSelector are supported
|
||||||
|
return typeof currAction === 'function'
|
||||||
|
? bindActionCreators(currAction, dispatch)
|
||||||
|
: bindActionCreators(currAction, dispatch);
|
||||||
|
}, [dispatch, currAction]);
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStoreState = useTypedSelector;
|
||||||
|
export const useStore = (): Store<ReactFlowState, ReactFlowAction> => {
|
||||||
|
const store = useStoreRedux<ReactFlowState, ReactFlowAction>();
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
export const useDispatch: ReactFlowDispatch = reduxUseDispatch;
|
||||||
|
|||||||
+9
-485
@@ -1,161 +1,15 @@
|
|||||||
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
|
import configureStore from './configure-store';
|
||||||
import isEqual from 'fast-deep-equal';
|
|
||||||
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
|
||||||
|
|
||||||
import { clampPosition, getDimensions } from '../utils';
|
import { ReactFlowState, ConnectionMode } from '../types';
|
||||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge, parseElement } from '../utils/graph';
|
|
||||||
import { getHandleBounds } from '../components/Nodes/utils';
|
|
||||||
|
|
||||||
import {
|
export const initialState: ReactFlowState = {
|
||||||
ElementId,
|
|
||||||
Elements,
|
|
||||||
Transform,
|
|
||||||
Node,
|
|
||||||
Edge,
|
|
||||||
Rect,
|
|
||||||
Dimensions,
|
|
||||||
XYPosition,
|
|
||||||
OnConnectFunc,
|
|
||||||
OnConnectStartFunc,
|
|
||||||
OnConnectStopFunc,
|
|
||||||
OnConnectEndFunc,
|
|
||||||
SelectionRect,
|
|
||||||
HandleType,
|
|
||||||
SetConnectionId,
|
|
||||||
NodePosUpdate,
|
|
||||||
NodeDiffUpdate,
|
|
||||||
TranslateExtent,
|
|
||||||
SnapGrid,
|
|
||||||
ConnectionMode,
|
|
||||||
NodeExtent,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
type NodeDimensionUpdate = {
|
|
||||||
id: ElementId;
|
|
||||||
nodeElement: HTMLDivElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NodeDimensionUpdates = {
|
|
||||||
updates: NodeDimensionUpdate[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type InitD3Zoom = {
|
|
||||||
d3Zoom: ZoomBehavior<Element, unknown>;
|
|
||||||
d3Selection: D3Selection<Element, unknown, null, undefined>;
|
|
||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
|
||||||
transform: Transform;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface StoreModel {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
transform: Transform;
|
|
||||||
elements: Elements;
|
|
||||||
nodes: Computed<StoreModel, Node[]>;
|
|
||||||
edges: Computed<StoreModel, Edge[]>;
|
|
||||||
selectedElements: Elements | null;
|
|
||||||
selectedNodesBbox: Rect;
|
|
||||||
viewportBox: Computed<StoreModel, Rect>;
|
|
||||||
|
|
||||||
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
|
||||||
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
|
||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
|
||||||
minZoom: number;
|
|
||||||
maxZoom: number;
|
|
||||||
translateExtent: TranslateExtent;
|
|
||||||
nodeExtent: NodeExtent;
|
|
||||||
|
|
||||||
nodesSelectionActive: boolean;
|
|
||||||
selectionActive: boolean;
|
|
||||||
|
|
||||||
userSelectionRect: SelectionRect;
|
|
||||||
|
|
||||||
connectionNodeId: ElementId | null;
|
|
||||||
connectionHandleId: ElementId | null;
|
|
||||||
connectionHandleType: HandleType | null;
|
|
||||||
connectionPosition: XYPosition;
|
|
||||||
connectionMode: ConnectionMode;
|
|
||||||
|
|
||||||
snapToGrid: boolean;
|
|
||||||
snapGrid: SnapGrid;
|
|
||||||
|
|
||||||
nodesDraggable: boolean;
|
|
||||||
nodesConnectable: boolean;
|
|
||||||
elementsSelectable: boolean;
|
|
||||||
|
|
||||||
multiSelectionActive: boolean;
|
|
||||||
|
|
||||||
reactFlowVersion: string;
|
|
||||||
|
|
||||||
onConnect?: OnConnectFunc;
|
|
||||||
onConnectStart?: OnConnectStartFunc;
|
|
||||||
onConnectStop?: OnConnectStopFunc;
|
|
||||||
onConnectEnd?: OnConnectEndFunc;
|
|
||||||
|
|
||||||
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
|
||||||
setOnConnectStart: Action<StoreModel, OnConnectStartFunc>;
|
|
||||||
setOnConnectStop: Action<StoreModel, OnConnectStopFunc>;
|
|
||||||
setOnConnectEnd: Action<StoreModel, OnConnectEndFunc>;
|
|
||||||
|
|
||||||
setElements: Action<StoreModel, Elements>;
|
|
||||||
|
|
||||||
batchUpdateNodeDimensions: Action<StoreModel, NodeDimensionUpdates>;
|
|
||||||
updateNodeDimensions: Action<StoreModel, NodeDimensionUpdate>;
|
|
||||||
|
|
||||||
updateNodePos: Action<StoreModel, NodePosUpdate>;
|
|
||||||
updateNodePosDiff: Action<StoreModel, NodeDiffUpdate>;
|
|
||||||
|
|
||||||
setSelection: Action<StoreModel, boolean>;
|
|
||||||
|
|
||||||
unsetNodesSelection: Action<StoreModel>;
|
|
||||||
resetSelectedElements: Action<StoreModel>;
|
|
||||||
|
|
||||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
|
||||||
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
|
||||||
|
|
||||||
updateTransform: Action<StoreModel, Transform>;
|
|
||||||
|
|
||||||
updateSize: Action<StoreModel, Dimensions>;
|
|
||||||
|
|
||||||
initD3Zoom: Action<StoreModel, InitD3Zoom>;
|
|
||||||
|
|
||||||
setMinZoom: Action<StoreModel, number>;
|
|
||||||
setMaxZoom: Action<StoreModel, number>;
|
|
||||||
|
|
||||||
setTranslateExtent: Action<StoreModel, TranslateExtent>;
|
|
||||||
setNodeExtent: Action<StoreModel, NodeExtent>;
|
|
||||||
|
|
||||||
setSnapToGrid: Action<StoreModel, boolean>;
|
|
||||||
setSnapGrid: Action<StoreModel, SnapGrid>;
|
|
||||||
|
|
||||||
setConnectionPosition: Action<StoreModel, XYPosition>;
|
|
||||||
|
|
||||||
setConnectionNodeId: Action<StoreModel, SetConnectionId>;
|
|
||||||
|
|
||||||
setInteractive: Action<StoreModel, boolean>;
|
|
||||||
setNodesDraggable: Action<StoreModel, boolean>;
|
|
||||||
setNodesConnectable: Action<StoreModel, boolean>;
|
|
||||||
setElementsSelectable: Action<StoreModel, boolean>;
|
|
||||||
|
|
||||||
setUserSelection: Action<StoreModel, XYPosition>;
|
|
||||||
updateUserSelection: Action<StoreModel, XYPosition>;
|
|
||||||
unsetUserSelection: Action<StoreModel>;
|
|
||||||
|
|
||||||
setMultiSelectionActive: Action<StoreModel, boolean>;
|
|
||||||
|
|
||||||
setConnectionMode: Action<StoreModel, ConnectionMode>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const storeModel: StoreModel = {
|
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
transform: [0, 0, 1],
|
transform: [0, 0, 1],
|
||||||
elements: [],
|
nodes: [],
|
||||||
nodes: computed((state) => state.elements.filter(isNode)),
|
edges: [],
|
||||||
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 },
|
||||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
|
||||||
|
|
||||||
d3Zoom: null,
|
d3Zoom: null,
|
||||||
d3Selection: null,
|
d3Selection: null,
|
||||||
@@ -166,6 +20,7 @@ export const storeModel: StoreModel = {
|
|||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
],
|
],
|
||||||
|
|
||||||
nodeExtent: [
|
nodeExtent: [
|
||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
@@ -199,341 +54,10 @@ export const storeModel: StoreModel = {
|
|||||||
multiSelectionActive: false,
|
multiSelectionActive: false,
|
||||||
|
|
||||||
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
|
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
|
||||||
|
|
||||||
setOnConnect: action((state, onConnect) => {
|
|
||||||
state.onConnect = onConnect;
|
|
||||||
}),
|
|
||||||
setOnConnectStart: action((state, onConnectStart) => {
|
|
||||||
state.onConnectStart = onConnectStart;
|
|
||||||
}),
|
|
||||||
setOnConnectStop: action((state, onConnectStop) => {
|
|
||||||
state.onConnectStop = onConnectStop;
|
|
||||||
}),
|
|
||||||
setOnConnectEnd: action((state, onConnectEnd) => {
|
|
||||||
state.onConnectEnd = onConnectEnd;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setElements: action((state, propElements) => {
|
|
||||||
// remove deleted elements
|
|
||||||
for (let i = 0; i < state.elements.length; i++) {
|
|
||||||
const se = state.elements[i];
|
|
||||||
const elementExistsInProps = propElements.find((pe) => pe.id === se.id);
|
|
||||||
|
|
||||||
if (!elementExistsInProps) {
|
|
||||||
state.elements.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
propElements.forEach((el) => {
|
|
||||||
const storeElementIndex = state.elements.findIndex((se) => se.id === el.id);
|
|
||||||
|
|
||||||
// update existing element
|
|
||||||
if (storeElementIndex !== -1) {
|
|
||||||
const storeElement = state.elements[storeElementIndex];
|
|
||||||
|
|
||||||
if (isNode(storeElement)) {
|
|
||||||
const propNode = el as Node;
|
|
||||||
const positionChanged =
|
|
||||||
storeElement.position.x !== propNode.position.x || storeElement.position.y !== propNode.position.y;
|
|
||||||
const typeChanged = typeof propNode.type !== 'undefined' && propNode.type !== storeElement.type;
|
|
||||||
|
|
||||||
state.elements[storeElementIndex] = {
|
|
||||||
...storeElement,
|
|
||||||
...propNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (positionChanged) {
|
|
||||||
(state.elements[storeElementIndex] as Node).__rf.position = clampPosition(
|
|
||||||
propNode.position,
|
|
||||||
state.nodeExtent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeChanged) {
|
|
||||||
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
|
|
||||||
// When the type of a node changes it is possible that the number or positions of handles changes too.
|
|
||||||
(state.elements[storeElementIndex] as Node).__rf.width = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.elements[storeElementIndex] = {
|
|
||||||
...storeElement,
|
|
||||||
...el,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// add new element
|
|
||||||
state.elements.push(parseElement(el, state.nodeExtent));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
batchUpdateNodeDimensions: action((state, { updates }) => {
|
|
||||||
updates.forEach((update) => {
|
|
||||||
const dimensions = getDimensions(update.nodeElement);
|
|
||||||
const matchingIndex = state.elements.findIndex((n) => n.id === update.id);
|
|
||||||
const matchingNode = state.elements[matchingIndex] as Node;
|
|
||||||
|
|
||||||
if (
|
|
||||||
matchingIndex !== -1 &&
|
|
||||||
dimensions.width &&
|
|
||||||
dimensions.height &&
|
|
||||||
(matchingNode.__rf.width !== dimensions.width || matchingNode.__rf.height !== dimensions.height)
|
|
||||||
) {
|
|
||||||
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
|
|
||||||
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.width = dimensions.width;
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.height = dimensions.height;
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.handleBounds = handleBounds;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateNodeDimensions: action((state, { id, nodeElement }) => {
|
|
||||||
const dimensions = getDimensions(nodeElement);
|
|
||||||
const matchingIndex = state.elements.findIndex((n) => n.id === id);
|
|
||||||
|
|
||||||
if (matchingIndex !== -1 && dimensions.width && dimensions.height) {
|
|
||||||
const handleBounds = getHandleBounds(nodeElement, state.transform[2]);
|
|
||||||
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.width = dimensions.width;
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.height = dimensions.height;
|
|
||||||
(state.elements[matchingIndex] as Node).__rf.handleBounds = handleBounds;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateNodePos: action((state, { id, pos }) => {
|
|
||||||
let position: XYPosition = pos;
|
|
||||||
|
|
||||||
if (state.snapToGrid) {
|
|
||||||
const [gridSizeX, gridSizeY] = state.snapGrid;
|
|
||||||
position = {
|
|
||||||
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
|
||||||
y: gridSizeY * Math.round(pos.y / gridSizeY),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
state.elements.forEach((n) => {
|
|
||||||
if (n.id === id && isNode(n)) {
|
|
||||||
n.__rf.position = clampPosition(position, state.nodeExtent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateNodePosDiff: action((state, { id = null, diff = null, isDragging = true }) => {
|
|
||||||
state.elements.forEach((n) => {
|
|
||||||
if (isNode(n) && (id === n.id || state.selectedElements?.find((sNode) => sNode.id === n.id))) {
|
|
||||||
if (diff) {
|
|
||||||
const position = {
|
|
||||||
x: n.__rf.position.x + diff.x,
|
|
||||||
y: n.__rf.position.y + diff.y,
|
|
||||||
};
|
|
||||||
n.__rf.position = clampPosition(position, state.nodeExtent);
|
|
||||||
}
|
|
||||||
n.__rf.isDragging = isDragging;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
setUserSelection: action((state, mousePos) => {
|
|
||||||
state.userSelectionRect = {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
startX: mousePos.x,
|
|
||||||
startY: mousePos.y,
|
|
||||||
x: mousePos.x,
|
|
||||||
y: mousePos.y,
|
|
||||||
draw: true,
|
|
||||||
};
|
|
||||||
state.selectionActive = true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateUserSelection: action((state, mousePos) => {
|
|
||||||
const startX = state.userSelectionRect.startX || 0;
|
|
||||||
const startY = state.userSelectionRect.startY || 0;
|
|
||||||
|
|
||||||
const negativeX = mousePos.x < startX;
|
|
||||||
const negativeY = mousePos.y < startY;
|
|
||||||
const nextRect = {
|
|
||||||
...state.userSelectionRect,
|
|
||||||
x: negativeX ? mousePos.x : state.userSelectionRect.x,
|
|
||||||
y: negativeY ? mousePos.y : state.userSelectionRect.y,
|
|
||||||
width: Math.abs(mousePos.x - startX),
|
|
||||||
height: Math.abs(mousePos.y - startY),
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedNodes = getNodesInside(state.nodes, nextRect, state.transform);
|
|
||||||
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
|
||||||
|
|
||||||
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
|
||||||
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
|
||||||
|
|
||||||
state.userSelectionRect = nextRect;
|
|
||||||
|
|
||||||
if (selectedElementsUpdated) {
|
|
||||||
state.selectedElements = nextSelectedElements.length > 0 ? nextSelectedElements : null;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
unsetUserSelection: action((state) => {
|
|
||||||
const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[];
|
|
||||||
|
|
||||||
if (!selectedNodes || selectedNodes.length === 0) {
|
|
||||||
state.selectionActive = false;
|
|
||||||
state.userSelectionRect.draw = false;
|
|
||||||
state.nodesSelectionActive = false;
|
|
||||||
state.selectedElements = null;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
|
||||||
|
|
||||||
state.nodesSelectionActive = true;
|
|
||||||
state.selectedNodesBbox = selectedNodesBbox;
|
|
||||||
|
|
||||||
state.userSelectionRect.draw = false;
|
|
||||||
state.selectionActive = false;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSelection: action((state, isActive) => {
|
|
||||||
state.selectionActive = isActive;
|
|
||||||
}),
|
|
||||||
|
|
||||||
unsetNodesSelection: action((state) => {
|
|
||||||
state.nodesSelectionActive = false;
|
|
||||||
}),
|
|
||||||
|
|
||||||
resetSelectedElements: action((state) => {
|
|
||||||
state.selectedElements = null;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSelectedElements: action((state, elements) => {
|
|
||||||
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
|
||||||
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
|
|
||||||
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
|
|
||||||
|
|
||||||
state.selectedElements = selectedElements;
|
|
||||||
}),
|
|
||||||
|
|
||||||
addSelectedElements: thunk((actions, elements, helpers) => {
|
|
||||||
const { multiSelectionActive, selectedElements } = helpers.getState();
|
|
||||||
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
|
||||||
|
|
||||||
if (multiSelectionActive) {
|
|
||||||
const nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
|
|
||||||
actions.setSelectedElements(nextElements);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
actions.setSelectedElements(elements);
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateTransform: action((state, transform) => {
|
|
||||||
state.transform[0] = transform[0];
|
|
||||||
state.transform[1] = transform[1];
|
|
||||||
state.transform[2] = transform[2];
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateSize: action((state, size) => {
|
|
||||||
// when parent has no size we use these default values
|
|
||||||
// so that the calculations don't throw any errors
|
|
||||||
state.width = size.width || 500;
|
|
||||||
state.height = size.height || 500;
|
|
||||||
}),
|
|
||||||
|
|
||||||
initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler, transform }) => {
|
|
||||||
state.d3Zoom = d3Zoom;
|
|
||||||
state.d3Selection = d3Selection;
|
|
||||||
state.d3ZoomHandler = d3ZoomHandler;
|
|
||||||
|
|
||||||
state.transform[0] = transform[0];
|
|
||||||
state.transform[1] = transform[1];
|
|
||||||
state.transform[2] = transform[2];
|
|
||||||
}),
|
|
||||||
|
|
||||||
setMinZoom: action((state, minZoom) => {
|
|
||||||
state.minZoom = minZoom;
|
|
||||||
|
|
||||||
if (state.d3Zoom) {
|
|
||||||
state.d3Zoom.scaleExtent([minZoom, state.maxZoom]);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
setMaxZoom: action((state, maxZoom) => {
|
|
||||||
state.maxZoom = maxZoom;
|
|
||||||
|
|
||||||
if (state.d3Zoom) {
|
|
||||||
state.d3Zoom.scaleExtent([state.minZoom, maxZoom]);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
setTranslateExtent: action((state, translateExtent) => {
|
|
||||||
state.translateExtent = translateExtent;
|
|
||||||
|
|
||||||
if (state.d3Zoom) {
|
|
||||||
state.d3Zoom.translateExtent(translateExtent);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
setNodeExtent: action((state, nodeExtent) => {
|
|
||||||
state.nodeExtent = nodeExtent;
|
|
||||||
|
|
||||||
state.elements.forEach((el) => {
|
|
||||||
if (isNode(el)) {
|
|
||||||
el.__rf.position = clampPosition(el.__rf.position, nodeExtent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
setConnectionPosition: action((state, position) => {
|
|
||||||
state.connectionPosition = position;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setConnectionNodeId: action((state, { connectionNodeId, connectionHandleId, connectionHandleType }) => {
|
|
||||||
state.connectionNodeId = connectionNodeId;
|
|
||||||
state.connectionHandleId = connectionHandleId;
|
|
||||||
state.connectionHandleType = connectionHandleType;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSnapToGrid: action((state, snapToGrid) => {
|
|
||||||
state.snapToGrid = snapToGrid;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSnapGrid: action((state, snapGrid) => {
|
|
||||||
state.snapGrid[0] = snapGrid[0];
|
|
||||||
state.snapGrid[1] = snapGrid[1];
|
|
||||||
}),
|
|
||||||
|
|
||||||
setInteractive: action((state, isInteractive) => {
|
|
||||||
state.nodesDraggable = isInteractive;
|
|
||||||
state.nodesConnectable = isInteractive;
|
|
||||||
state.elementsSelectable = isInteractive;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setNodesDraggable: action((state, nodesDraggable) => {
|
|
||||||
state.nodesDraggable = nodesDraggable;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setNodesConnectable: action((state, nodesConnectable) => {
|
|
||||||
state.nodesConnectable = nodesConnectable;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setElementsSelectable: action((state, elementsSelectable) => {
|
|
||||||
state.elementsSelectable = elementsSelectable;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setMultiSelectionActive: action((state, isActive) => {
|
|
||||||
state.multiSelectionActive = isActive;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setConnectionMode: action((state, connectionMode) => {
|
|
||||||
state.connectionMode = connectionMode;
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;
|
const store = configureStore(initialState);
|
||||||
const store = createStore(storeModel, { devTools: nodeEnv === 'development' });
|
|
||||||
|
export type ReactFlowDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
|
import { clampPosition, getDimensions } from '../utils';
|
||||||
|
import {
|
||||||
|
getNodesInside,
|
||||||
|
getConnectedEdges,
|
||||||
|
getRectOfNodes,
|
||||||
|
isNode,
|
||||||
|
isEdge,
|
||||||
|
parseNode,
|
||||||
|
parseEdge,
|
||||||
|
} from '../utils/graph';
|
||||||
|
import { getHandleBounds } from '../components/Nodes/utils';
|
||||||
|
|
||||||
|
import { ReactFlowState, Node, XYPosition, Edge } from '../types';
|
||||||
|
import * as constants from './contants';
|
||||||
|
import { ReactFlowAction } from './actions';
|
||||||
|
|
||||||
|
import { initialState } from './index';
|
||||||
|
|
||||||
|
type NextElements = {
|
||||||
|
nextNodes: Node[];
|
||||||
|
nextEdges: Edge[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
|
||||||
|
switch (action.type) {
|
||||||
|
case constants.SET_ELEMENTS: {
|
||||||
|
const propElements = action.payload;
|
||||||
|
const nextElements: NextElements = {
|
||||||
|
nextNodes: [],
|
||||||
|
nextEdges: [],
|
||||||
|
};
|
||||||
|
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
||||||
|
if (isNode(propElement)) {
|
||||||
|
const storeNode = state.nodes.find((node) => node.id === propElement.id);
|
||||||
|
|
||||||
|
if (storeNode) {
|
||||||
|
const updatedNode: Node = {
|
||||||
|
...storeNode,
|
||||||
|
...propElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
|
||||||
|
updatedNode.__rf.position = propElement.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
|
||||||
|
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
|
||||||
|
// When the type of a node changes it is possible that the number or positions of handles changes too.
|
||||||
|
updatedNode.__rf.width = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.nextNodes.push(updatedNode);
|
||||||
|
} else {
|
||||||
|
res.nextNodes.push(parseNode(propElement, state.nodeExtent));
|
||||||
|
}
|
||||||
|
} else if (isEdge(propElement)) {
|
||||||
|
const storeEdge = state.edges.find((se) => se.id === propElement.id);
|
||||||
|
|
||||||
|
if (storeEdge) {
|
||||||
|
res.nextEdges.push({
|
||||||
|
...storeEdge,
|
||||||
|
...propElement,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.nextEdges.push(parseEdge(propElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, nextElements);
|
||||||
|
|
||||||
|
return { ...state, nodes: nextNodes, edges: nextEdges };
|
||||||
|
}
|
||||||
|
case constants.UPDATE_NODE_DIMENSIONS: {
|
||||||
|
const updatedNodes = state.nodes.map((node) => {
|
||||||
|
const update = action.payload.find((u) => u.id === node.id);
|
||||||
|
if (update) {
|
||||||
|
const dimensions = getDimensions(update.nodeElement);
|
||||||
|
|
||||||
|
if (
|
||||||
|
dimensions.width &&
|
||||||
|
dimensions.height &&
|
||||||
|
(node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height)
|
||||||
|
) {
|
||||||
|
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
__rf: {
|
||||||
|
...node.__rf,
|
||||||
|
...dimensions,
|
||||||
|
handleBounds,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: updatedNodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.UPDATE_NODE_POS: {
|
||||||
|
const { id, pos } = action.payload;
|
||||||
|
let position: XYPosition = pos;
|
||||||
|
|
||||||
|
if (state.snapToGrid) {
|
||||||
|
const [gridSizeX, gridSizeY] = state.snapGrid;
|
||||||
|
position = {
|
||||||
|
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
||||||
|
y: gridSizeY * Math.round(pos.y / gridSizeY),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNodes = state.nodes.map((node) => {
|
||||||
|
if (node.id === id) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
__rf: {
|
||||||
|
...node.__rf,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...state, nodes: nextNodes };
|
||||||
|
}
|
||||||
|
case constants.UPDATE_NODE_POS_DIFF: {
|
||||||
|
const { id, diff, isDragging } = action.payload;
|
||||||
|
|
||||||
|
const nextNodes = state.nodes.map((node) => {
|
||||||
|
if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) {
|
||||||
|
const updatedNode = {
|
||||||
|
...node,
|
||||||
|
__rf: {
|
||||||
|
...node.__rf,
|
||||||
|
isDragging,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (diff) {
|
||||||
|
updatedNode.__rf.position = {
|
||||||
|
x: node.__rf.position.x + diff.x,
|
||||||
|
y: node.__rf.position.y + diff.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...state, nodes: nextNodes };
|
||||||
|
}
|
||||||
|
case constants.SET_USER_SELECTION: {
|
||||||
|
const mousePos = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectionActive: true,
|
||||||
|
userSelectionRect: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
startX: mousePos.x,
|
||||||
|
startY: mousePos.y,
|
||||||
|
x: mousePos.x,
|
||||||
|
y: mousePos.y,
|
||||||
|
draw: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.UPDATE_USER_SELECTION: {
|
||||||
|
const mousePos = action.payload;
|
||||||
|
const startX = state.userSelectionRect.startX ?? 0;
|
||||||
|
const startY = state.userSelectionRect.startY ?? 0;
|
||||||
|
|
||||||
|
const nextUserSelectRect = {
|
||||||
|
...state.userSelectionRect,
|
||||||
|
x: mousePos.x < startX ? mousePos.x : state.userSelectionRect.x,
|
||||||
|
y: mousePos.y < startY ? mousePos.y : state.userSelectionRect.y,
|
||||||
|
width: Math.abs(mousePos.x - startX),
|
||||||
|
height: Math.abs(mousePos.y - startY),
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform);
|
||||||
|
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
||||||
|
|
||||||
|
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
||||||
|
const selectedElementsChanged = !isEqual(nextSelectedElements, state.selectedElements);
|
||||||
|
const selectedElementsUpdate = selectedElementsChanged
|
||||||
|
? {
|
||||||
|
selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...selectedElementsUpdate,
|
||||||
|
userSelectionRect: nextUserSelectRect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.UNSET_USER_SELECTION: {
|
||||||
|
const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[];
|
||||||
|
|
||||||
|
const stateUpdate = {
|
||||||
|
...state,
|
||||||
|
selectionActive: false,
|
||||||
|
userSelectionRect: {
|
||||||
|
...state.userSelectionRect,
|
||||||
|
draw: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedNodes || selectedNodes.length === 0) {
|
||||||
|
stateUpdate.selectedElements = null;
|
||||||
|
stateUpdate.nodesSelectionActive = false;
|
||||||
|
} else {
|
||||||
|
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
||||||
|
stateUpdate.selectedNodesBbox = selectedNodesBbox;
|
||||||
|
stateUpdate.nodesSelectionActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateUpdate;
|
||||||
|
}
|
||||||
|
case constants.SET_SELECTED_ELEMENTS: {
|
||||||
|
const elements = action.payload;
|
||||||
|
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
||||||
|
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
|
||||||
|
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedElements,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.ADD_SELECTED_ELEMENTS: {
|
||||||
|
const { multiSelectionActive, selectedElements } = state;
|
||||||
|
const elements = action.payload;
|
||||||
|
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
||||||
|
|
||||||
|
let nextElements = selectedElementsArr;
|
||||||
|
|
||||||
|
if (multiSelectionActive) {
|
||||||
|
nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements);
|
||||||
|
const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements;
|
||||||
|
|
||||||
|
return { ...state, selectedElements: nextSelectedElements };
|
||||||
|
}
|
||||||
|
case constants.INIT_D3ZOOM: {
|
||||||
|
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
d3Zoom,
|
||||||
|
d3Selection,
|
||||||
|
d3ZoomHandler,
|
||||||
|
transform,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.SET_MINZOOM: {
|
||||||
|
const minZoom = action.payload;
|
||||||
|
|
||||||
|
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
minZoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case constants.SET_MAXZOOM: {
|
||||||
|
const maxZoom = action.payload;
|
||||||
|
|
||||||
|
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
maxZoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.SET_TRANSLATEEXTENT: {
|
||||||
|
const translateExtent = action.payload;
|
||||||
|
|
||||||
|
state.d3Zoom?.translateExtent(translateExtent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
translateExtent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.SET_NODE_EXTENT: {
|
||||||
|
const nodeExtent = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodeExtent,
|
||||||
|
nodes: state.nodes.map((node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
__rf: {
|
||||||
|
...node.__rf,
|
||||||
|
position: clampPosition(node.__rf.position, nodeExtent),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case constants.SET_ON_CONNECT:
|
||||||
|
case constants.SET_ON_CONNECT_START:
|
||||||
|
case constants.SET_ON_CONNECT_STOP:
|
||||||
|
case constants.SET_ON_CONNECT_END:
|
||||||
|
case constants.RESET_SELECTED_ELEMENTS:
|
||||||
|
case constants.UNSET_NODES_SELECTION:
|
||||||
|
case constants.UPDATE_TRANSFORM:
|
||||||
|
case constants.UPDATE_SIZE:
|
||||||
|
case constants.SET_CONNECTION_POSITION:
|
||||||
|
case constants.SET_CONNECTION_NODEID:
|
||||||
|
case constants.SET_SNAPTOGRID:
|
||||||
|
case constants.SET_SNAPGRID:
|
||||||
|
case constants.SET_INTERACTIVE:
|
||||||
|
case constants.SET_NODES_DRAGGABLE:
|
||||||
|
case constants.SET_NODES_CONNECTABLE:
|
||||||
|
case constants.SET_ELEMENTS_SELECTABLE:
|
||||||
|
case constants.SET_MULTI_SELECTION_ACTIVE:
|
||||||
|
case constants.SET_CONNECTION_MODE:
|
||||||
|
return { ...state, ...action.payload };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function createAction<T extends string>(type: T): { type: T };
|
||||||
|
export function createAction<T extends string, P extends any>(type: T, payload: P): { type: T; payload: P };
|
||||||
|
export function createAction(type: string, payload?: any) {
|
||||||
|
return { type, payload };
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
|
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
||||||
|
|
||||||
export type ElementId = string;
|
export type ElementId = string;
|
||||||
|
|
||||||
@@ -360,7 +361,65 @@ export interface ZoomPanHelperFunctions {
|
|||||||
fitView: FitViewFunc;
|
fitView: FitViewFunc;
|
||||||
setCenter: (x: number, y: number, zoom?: number) => void;
|
setCenter: (x: number, y: number, zoom?: number) => void;
|
||||||
fitBounds: (bounds: Rect, padding?: number) => void;
|
fitBounds: (bounds: Rect, padding?: number) => void;
|
||||||
|
project: (position: XYPosition) => XYPosition;
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
|
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
|
||||||
|
|
||||||
|
export type NodeDimensionUpdate = {
|
||||||
|
id: ElementId;
|
||||||
|
nodeElement: HTMLDivElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InitD3ZoomPayload = {
|
||||||
|
d3Zoom: ZoomBehavior<Element, unknown>;
|
||||||
|
d3Selection: D3Selection<Element, unknown, null, undefined>;
|
||||||
|
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||||
|
transform: Transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReactFlowState {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
transform: Transform;
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
selectedElements: Elements | null;
|
||||||
|
selectedNodesBbox: Rect;
|
||||||
|
|
||||||
|
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
||||||
|
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
||||||
|
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||||
|
minZoom: number;
|
||||||
|
maxZoom: number;
|
||||||
|
translateExtent: TranslateExtent;
|
||||||
|
nodeExtent: NodeExtent;
|
||||||
|
|
||||||
|
nodesSelectionActive: boolean;
|
||||||
|
selectionActive: boolean;
|
||||||
|
|
||||||
|
userSelectionRect: SelectionRect;
|
||||||
|
|
||||||
|
connectionNodeId: ElementId | null;
|
||||||
|
connectionHandleId: ElementId | null;
|
||||||
|
connectionHandleType: HandleType | null;
|
||||||
|
connectionPosition: XYPosition;
|
||||||
|
connectionMode: ConnectionMode;
|
||||||
|
|
||||||
|
snapToGrid: boolean;
|
||||||
|
snapGrid: SnapGrid;
|
||||||
|
|
||||||
|
nodesDraggable: boolean;
|
||||||
|
nodesConnectable: boolean;
|
||||||
|
elementsSelectable: boolean;
|
||||||
|
|
||||||
|
multiSelectionActive: boolean;
|
||||||
|
|
||||||
|
reactFlowVersion: string;
|
||||||
|
|
||||||
|
onConnect?: OnConnectFunc;
|
||||||
|
onConnectStart?: OnConnectStartFunc;
|
||||||
|
onConnectStop?: OnConnectStopFunc;
|
||||||
|
onConnectEnd?: OnConnectEndFunc;
|
||||||
|
}
|
||||||
|
|||||||
+24
-27
@@ -1,7 +1,7 @@
|
|||||||
import { Store } from 'easy-peasy';
|
import { Store } from 'redux';
|
||||||
|
|
||||||
import { StoreModel } from '../store';
|
|
||||||
import { clampPosition } from '../utils';
|
import { clampPosition } from '../utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElementId,
|
ElementId,
|
||||||
Node,
|
Node,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Connection,
|
Connection,
|
||||||
FlowExportObject,
|
FlowExportObject,
|
||||||
|
ReactFlowState,
|
||||||
NodeExtent,
|
NodeExtent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ export const pointToRendererPoint = (
|
|||||||
return position;
|
return position;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onLoadProject = (currentStore: Store<StoreModel>) => {
|
export const onLoadProject = (currentStore: Store<ReactFlowState>) => {
|
||||||
return (position: XYPosition): XYPosition => {
|
return (position: XYPosition): XYPosition => {
|
||||||
const { transform, snapToGrid, snapGrid } = currentStore.getState();
|
const { transform, snapToGrid, snapGrid } = currentStore.getState();
|
||||||
|
|
||||||
@@ -145,35 +146,31 @@ export const onLoadProject = (currentStore: Store<StoreModel>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseElement = (element: Node | Edge, nodeExtent: NodeExtent): Node | Edge => {
|
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
|
||||||
if (!element.id) {
|
|
||||||
throw new Error('All nodes and edges need to have an id.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEdge(element)) {
|
|
||||||
return {
|
|
||||||
...element,
|
|
||||||
source: element.source.toString(),
|
|
||||||
target: element.target.toString(),
|
|
||||||
sourceHandle: element.sourceHandle ? element.sourceHandle.toString() : null,
|
|
||||||
targetHandle: element.targetHandle ? element.targetHandle.toString() : null,
|
|
||||||
id: element.id.toString(),
|
|
||||||
type: element.type || 'default',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...element,
|
...node,
|
||||||
id: element.id.toString(),
|
id: node.id.toString(),
|
||||||
type: element.type || 'default',
|
type: node.type || 'default',
|
||||||
__rf: {
|
__rf: {
|
||||||
position: clampPosition(element.position, nodeExtent),
|
position: clampPosition(node.position, nodeExtent),
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
handleBounds: {},
|
handleBounds: {},
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
},
|
},
|
||||||
} as Node;
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseEdge = (edge: Edge): Edge => {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
source: edge.source.toString(),
|
||||||
|
target: edge.target.toString(),
|
||||||
|
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
|
||||||
|
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
|
||||||
|
id: edge.id.toString(),
|
||||||
|
type: edge.type || 'default',
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||||
@@ -269,7 +266,7 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onLoadGetElements = (currentStore: Store<StoreModel>) => {
|
export const onLoadGetElements = (currentStore: Store<ReactFlowState>) => {
|
||||||
return (): Elements => {
|
return (): Elements => {
|
||||||
const { nodes = [], edges = [] } = currentStore.getState();
|
const { nodes = [], edges = [] } = currentStore.getState();
|
||||||
|
|
||||||
@@ -277,7 +274,7 @@ export const onLoadGetElements = (currentStore: Store<StoreModel>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onLoadToObject = (currentStore: Store<StoreModel>) => {
|
export const onLoadToObject = (currentStore: Store<ReactFlowState>) => {
|
||||||
return (): FlowExportObject => {
|
return (): FlowExportObject => {
|
||||||
const { nodes = [], edges = [], transform } = currentStore.getState();
|
const { nodes = [], edges = [], transform } = currentStore.getState();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user