Graph Prop: isInteractive (#56)
* feat(props): add isInteractive * refactor(selection): dont allow if not interactive * feat(renderer): add class if is interactive * chore(inactive): add tests * refactor(inactive): no connection line, no edge selection closes #49
This commit is contained in:
@@ -52,6 +52,7 @@ const BasicGraph = () => (
|
|||||||
- `snapToGrid`: default: `false`
|
- `snapToGrid`: default: `false`
|
||||||
- `snapGrid`: [x, y] array - default: `[16, 16]`
|
- `snapGrid`: [x, y] array - default: `[16, 16]`
|
||||||
- `onlyRenderVisibleNodes`: default: `true`
|
- `onlyRenderVisibleNodes`: default: `true`
|
||||||
|
- `isInteractive`: default: `true`. If the graph is not interactive you can't drag any nodes
|
||||||
|
|
||||||
## Nodes
|
## Nodes
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
describe('Inactive Graph Rendering', () => {
|
||||||
|
it('renders a graph', () => {
|
||||||
|
cy.visit('/inactive');
|
||||||
|
|
||||||
|
cy.get('.react-flow__renderer');
|
||||||
|
cy.get('.react-flow__node').should('have.length', 4);
|
||||||
|
cy.get('.react-flow__edge').should('have.length', 2);
|
||||||
|
cy.get('.react-flow__node')
|
||||||
|
.children('div')
|
||||||
|
.children('.react-flow__handle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to select a node by click', () => {
|
||||||
|
cy.get('.react-flow__node:first')
|
||||||
|
.click()
|
||||||
|
.should('have.not.class', 'selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to select an edge by click', () => {
|
||||||
|
cy.get('.react-flow__edge:first')
|
||||||
|
.click()
|
||||||
|
.should('have.not.class', 'selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to do a selection', () => {
|
||||||
|
cy.get('body')
|
||||||
|
.type('{shift}', { release: false })
|
||||||
|
.get('.react-flow__selectionpane')
|
||||||
|
.should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to drag a node', () => {
|
||||||
|
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||||
|
|
||||||
|
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then($el => {
|
||||||
|
const styleAfterDrag = $el.css('transform');
|
||||||
|
expect(styleBeforeDrag).to.equal(styleAfterDrag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to connect nodes', () => {
|
||||||
|
cy.get('.react-flow__node')
|
||||||
|
.contains('Node 3')
|
||||||
|
.find('.react-flow__handle.source')
|
||||||
|
.trigger('mousedown', { which: 1 });
|
||||||
|
|
||||||
|
cy.get('.react-flow__node')
|
||||||
|
.contains('Node 4')
|
||||||
|
.find('.react-flow__handle.target')
|
||||||
|
.trigger('mousemove')
|
||||||
|
.trigger('mouseup', { force: true });
|
||||||
|
|
||||||
|
cy.get('.react-flow__edge').should('have.length', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles interactive mode', () => {
|
||||||
|
cy.get('.react-flow__interactive').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects a node by click', () => {
|
||||||
|
cy.get('.react-flow__node:first')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects an edge by click', () => {
|
||||||
|
cy.get('.react-flow__edge:first')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+1110
-82
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1110
-82
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-15
@@ -1,16 +1,4 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React from 'react';
|
||||||
import { Node, Transform, ElementId, NodeComponentProps } from '../../types';
|
import { NodeComponentProps, WrapNodeProps } from '../../types';
|
||||||
interface WrapNodeProps {
|
declare const _default: (NodeComponent: React.ComponentType<NodeComponentProps>) => React.MemoExoticComponent<({ id, type, data, transform, xPos, yPos, selected, onClick, onNodeDragStop, style, isInteractive, }: WrapNodeProps) => JSX.Element>;
|
||||||
id: ElementId;
|
|
||||||
type: string;
|
|
||||||
data: any;
|
|
||||||
selected: boolean;
|
|
||||||
transform: Transform;
|
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
onClick: (node: Node) => void | undefined;
|
|
||||||
onNodeDragStop: (node: Node) => void;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
declare const _default: (NodeComponent: React.ComponentType<NodeComponentProps>) => React.MemoExoticComponent<({ id, type, data, transform, xPos, yPos, selected, onClick, onNodeDragStop, style, }: WrapNodeProps) => JSX.Element>;
|
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
declare const _default: React.MemoExoticComponent<() => JSX.Element>;
|
declare const _default: React.MemoExoticComponent<() => JSX.Element | null>;
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
Vendored
+2
-1
@@ -21,6 +21,7 @@ export interface GraphViewProps {
|
|||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean;
|
onlyRenderVisibleNodes: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
}
|
}
|
||||||
declare const GraphView: React.MemoExoticComponent<({ nodeTypes, edgeTypes, onMove, onLoad, onElementClick, onNodeDragStop, connectionLineType, connectionLineStyle, selectionKeyCode, onElementsRemove, deleteKeyCode, elements, showBackground, backgroundGap, backgroundColor, backgroundType, onConnect, snapToGrid, snapGrid, onlyRenderVisibleNodes }: GraphViewProps) => JSX.Element>;
|
declare const GraphView: React.MemoExoticComponent<({ nodeTypes, edgeTypes, onMove, onLoad, onElementClick, onNodeDragStop, connectionLineType, connectionLineStyle, selectionKeyCode, onElementsRemove, deleteKeyCode, elements, showBackground, backgroundGap, backgroundColor, backgroundType, onConnect, snapToGrid, snapGrid, onlyRenderVisibleNodes, isInteractive, }: GraphViewProps) => JSX.Element>;
|
||||||
export default GraphView;
|
export default GraphView;
|
||||||
|
|||||||
Vendored
+3
-1
@@ -22,9 +22,10 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
|||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean;
|
onlyRenderVisibleNodes: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
}
|
}
|
||||||
declare const ReactFlow: {
|
declare const ReactFlow: {
|
||||||
({ style, onElementClick, elements, children, nodeTypes, edgeTypes, onLoad, onMove, onElementsRemove, onConnect, onNodeDragStop, connectionLineType, connectionLineStyle, deleteKeyCode, selectionKeyCode, showBackground, backgroundGap, backgroundType, backgroundColor, snapToGrid, snapGrid, onlyRenderVisibleNodes }: ReactFlowProps): JSX.Element;
|
({ style, onElementClick, elements, children, nodeTypes, edgeTypes, onLoad, onMove, onElementsRemove, onConnect, onNodeDragStop, connectionLineType, connectionLineStyle, deleteKeyCode, selectionKeyCode, showBackground, backgroundGap, backgroundType, backgroundColor, snapToGrid, snapGrid, onlyRenderVisibleNodes, isInteractive, }: ReactFlowProps): JSX.Element;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
onElementClick: () => void;
|
onElementClick: () => void;
|
||||||
@@ -54,6 +55,7 @@ declare const ReactFlow: {
|
|||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: number[];
|
snapGrid: number[];
|
||||||
onlyRenderVisibleNodes: boolean;
|
onlyRenderVisibleNodes: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export default ReactFlow;
|
export default ReactFlow;
|
||||||
|
|||||||
Vendored
+2
@@ -45,6 +45,7 @@ export declare const useStoreActions: <Result>(mapActions: (actions: import("eas
|
|||||||
}>;
|
}>;
|
||||||
setConnectionPosition: import("easy-peasy").Action<StoreModel, import("../types").XYPosition>;
|
setConnectionPosition: import("easy-peasy").Action<StoreModel, import("../types").XYPosition>;
|
||||||
setConnectionSourceId: import("easy-peasy").Action<StoreModel, string | null>;
|
setConnectionSourceId: import("easy-peasy").Action<StoreModel, string | null>;
|
||||||
|
setInteractive: import("easy-peasy").Action<StoreModel, boolean>;
|
||||||
}, "1">) => Result) => Result;
|
}, "1">) => Result) => Result;
|
||||||
export declare const useStoreDispatch: () => import("easy-peasy").Dispatch<StoreModel, import("redux").Action<any>>;
|
export declare const useStoreDispatch: () => import("easy-peasy").Dispatch<StoreModel, import("redux").Action<any>>;
|
||||||
export declare const useStoreState: <Result>(mapState: (state: import("easy-peasy").IntermediateStateMapper<{
|
export declare const useStoreState: <Result>(mapState: (state: import("easy-peasy").IntermediateStateMapper<{
|
||||||
@@ -65,5 +66,6 @@ export declare const useStoreState: <Result>(mapState: (state: import("easy-peas
|
|||||||
connectionPosition: import("../types").XYPosition;
|
connectionPosition: import("../types").XYPosition;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
|
isInteractive: boolean;
|
||||||
onConnect: import("../types").OnConnectFunc;
|
onConnect: import("../types").OnConnectFunc;
|
||||||
}, "1">) => Result, dependencies?: any[] | undefined) => Result;
|
}, "1">) => Result, dependencies?: any[] | undefined) => Result;
|
||||||
|
|||||||
Vendored
+5
@@ -49,6 +49,7 @@ export interface StoreModel {
|
|||||||
connectionPosition: XYPosition;
|
connectionPosition: XYPosition;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
|
isInteractive: boolean;
|
||||||
onConnect: OnConnectFunc;
|
onConnect: OnConnectFunc;
|
||||||
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
||||||
setNodes: Action<StoreModel, Node[]>;
|
setNodes: Action<StoreModel, Node[]>;
|
||||||
@@ -65,6 +66,7 @@ export interface StoreModel {
|
|||||||
setSnapGrid: Action<StoreModel, SetSnapGrid>;
|
setSnapGrid: Action<StoreModel, SetSnapGrid>;
|
||||||
setConnectionPosition: Action<StoreModel, XYPosition>;
|
setConnectionPosition: Action<StoreModel, XYPosition>;
|
||||||
setConnectionSourceId: Action<StoreModel, ElementId | null>;
|
setConnectionSourceId: Action<StoreModel, ElementId | null>;
|
||||||
|
setInteractive: Action<StoreModel, boolean>;
|
||||||
}
|
}
|
||||||
declare const store: {
|
declare const store: {
|
||||||
getState: () => import("easy-peasy").IntermediateStateMapper<{
|
getState: () => import("easy-peasy").IntermediateStateMapper<{
|
||||||
@@ -85,6 +87,7 @@ declare const store: {
|
|||||||
connectionPosition: XYPosition;
|
connectionPosition: XYPosition;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
|
isInteractive: boolean;
|
||||||
onConnect: OnConnectFunc;
|
onConnect: OnConnectFunc;
|
||||||
}, "1">;
|
}, "1">;
|
||||||
subscribe: (listener: () => void) => import("redux").Unsubscribe;
|
subscribe: (listener: () => void) => import("redux").Unsubscribe;
|
||||||
@@ -106,6 +109,7 @@ declare const store: {
|
|||||||
connectionPosition: XYPosition;
|
connectionPosition: XYPosition;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
|
isInteractive: boolean;
|
||||||
onConnect: OnConnectFunc;
|
onConnect: OnConnectFunc;
|
||||||
}, "1">, import("redux").AnyAction>) => void;
|
}, "1">, import("redux").AnyAction>) => void;
|
||||||
dispatch: import("easy-peasy").Dispatch<StoreModel, import("redux").Action<any>>;
|
dispatch: import("easy-peasy").Dispatch<StoreModel, import("redux").Action<any>>;
|
||||||
@@ -133,6 +137,7 @@ declare const store: {
|
|||||||
setSnapGrid: Action<StoreModel, SetSnapGrid>;
|
setSnapGrid: Action<StoreModel, SetSnapGrid>;
|
||||||
setConnectionPosition: Action<StoreModel, XYPosition>;
|
setConnectionPosition: Action<StoreModel, XYPosition>;
|
||||||
setConnectionSourceId: Action<StoreModel, string | null>;
|
setConnectionSourceId: Action<StoreModel, string | null>;
|
||||||
|
setInteractive: Action<StoreModel, boolean>;
|
||||||
}, "1">;
|
}, "1">;
|
||||||
getListeners: () => import("easy-peasy").ListenerMapper<{
|
getListeners: () => import("easy-peasy").ListenerMapper<{
|
||||||
selectedNodesBbox: Rect;
|
selectedNodesBbox: Rect;
|
||||||
|
|||||||
Vendored
+13
@@ -82,6 +82,19 @@ export interface NodeComponentProps {
|
|||||||
onNodeDragStop?: () => any;
|
onNodeDragStop?: () => any;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
export interface WrapNodeProps {
|
||||||
|
id: ElementId;
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
selected: boolean;
|
||||||
|
transform: Transform;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
isInteractive: boolean;
|
||||||
|
onClick: (node: Node) => void | undefined;
|
||||||
|
onNodeDragStop: (node: Node) => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
export declare type FitViewParams = {
|
export declare type FitViewParams = {
|
||||||
padding: number;
|
padding: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import Graph, { MiniMap, Controls } from 'react-flow';
|
||||||
|
|
||||||
|
const initialElements = [
|
||||||
|
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||||
|
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||||
|
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||||
|
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||||
|
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||||
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const EmptyGraph = () => {
|
||||||
|
const [isInteractive, setIsInteractive] = useState(false);
|
||||||
|
const onToggleInteractive = (evt) => {
|
||||||
|
setIsInteractive(evt.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Graph
|
||||||
|
elements={initialElements}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
backgroundType="lines"
|
||||||
|
isInteractive={isInteractive}
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
<div
|
||||||
|
style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
interactive
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isInteractive}
|
||||||
|
onChange={onToggleInteractive}
|
||||||
|
className="react-flow__interactive"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Graph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyGraph;
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
|||||||
import CustomNodes from './CustomNodes';
|
import CustomNodes from './CustomNodes';
|
||||||
import Basic from './Basic';
|
import Basic from './Basic';
|
||||||
import Empty from './Empty';
|
import Empty from './Empty';
|
||||||
|
import Inactive from './Inactive';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ ReactDOM.render((
|
|||||||
<Route path="/empty">
|
<Route path="/empty">
|
||||||
<Empty />
|
<Empty />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/inactive">
|
||||||
|
<Inactive />
|
||||||
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<CustomNodes />
|
<CustomNodes />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface ConnectionLineProps {
|
|||||||
connectionLineType?: string | null;
|
connectionLineType?: string | null;
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
transform: Transform;
|
transform: Transform;
|
||||||
|
isInteractive: boolean;
|
||||||
connectionLineStyle?: SVGAttributes<{}>;
|
connectionLineStyle?: SVGAttributes<{}>;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,7 @@ export default ({
|
|||||||
nodes = [],
|
nodes = [],
|
||||||
className,
|
className,
|
||||||
transform,
|
transform,
|
||||||
|
isInteractive,
|
||||||
}: ConnectionLineProps) => {
|
}: ConnectionLineProps) => {
|
||||||
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||||
const hasHandleId = connectionSourceId.includes('__');
|
const hasHandleId = connectionSourceId.includes('__');
|
||||||
@@ -35,23 +37,17 @@ export default ({
|
|||||||
setSourceNode(nextSourceNode);
|
setSourceNode(nextSourceNode);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode || !isInteractive) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const edgeClasses: string = cx('react-flow__edge', 'connection', className);
|
const edgeClasses: string = cx('react-flow__edge', 'connection', className);
|
||||||
|
|
||||||
const sourceHandle = handleId
|
const sourceHandle = handleId
|
||||||
? sourceNode.__rg.handleBounds.source.find(
|
? sourceNode.__rg.handleBounds.source.find((d: HandleElement) => d.id === handleId)
|
||||||
(d: HandleElement) => d.id === handleId
|
|
||||||
)
|
|
||||||
: sourceNode.__rg.handleBounds.source[0];
|
: sourceNode.__rg.handleBounds.source[0];
|
||||||
const sourceHandleX = sourceHandle
|
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rg.width / 2;
|
||||||
? sourceHandle.x + sourceHandle.width / 2
|
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rg.height;
|
||||||
: sourceNode.__rg.width / 2;
|
|
||||||
const sourceHandleY = sourceHandle
|
|
||||||
? sourceHandle.y + sourceHandle.height / 2
|
|
||||||
: sourceNode.__rg.height;
|
|
||||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||||
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
||||||
|
|
||||||
|
|||||||
@@ -13,23 +13,15 @@ interface EdgeWrapperProps {
|
|||||||
onClick: (edge: Edge) => void;
|
onClick: (edge: Edge) => void;
|
||||||
animated: boolean;
|
animated: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
|
export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
|
||||||
const EdgeWrapper = memo(
|
const EdgeWrapper = memo(
|
||||||
({
|
({ id, source, target, type, animated, selected, onClick, isInteractive, ...rest }: EdgeWrapperProps) => {
|
||||||
id,
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
animated,
|
|
||||||
selected,
|
|
||||||
onClick,
|
|
||||||
...rest
|
|
||||||
}: EdgeWrapperProps) => {
|
|
||||||
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
||||||
const onEdgeClick = (evt: MouseEvent): void => {
|
const onEdgeClick = (evt: MouseEvent): void => {
|
||||||
if (isInputDOMNode(evt)) {
|
if (isInputDOMNode(evt) || !isInteractive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import React, {
|
import React, { useEffect, useRef, useState, memo, ComponentType } from 'react';
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
memo,
|
|
||||||
ComponentType,
|
|
||||||
CSSProperties,
|
|
||||||
} from 'react';
|
|
||||||
import { DraggableCore, DraggableEvent } from 'react-draggable';
|
import { DraggableCore, DraggableEvent } from 'react-draggable';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ResizeObserver } from 'resize-observer';
|
import { ResizeObserver } from 'resize-observer';
|
||||||
@@ -21,21 +14,9 @@ import {
|
|||||||
Transform,
|
Transform,
|
||||||
ElementId,
|
ElementId,
|
||||||
NodeComponentProps,
|
NodeComponentProps,
|
||||||
|
WrapNodeProps,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
interface WrapNodeProps {
|
|
||||||
id: ElementId;
|
|
||||||
type: string;
|
|
||||||
data: any;
|
|
||||||
selected: boolean;
|
|
||||||
transform: Transform;
|
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
onClick: (node: Node) => void | undefined;
|
|
||||||
onNodeDragStop: (node: Node) => void;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHandle = (evt: MouseEvent | DraggableEvent) => {
|
const isHandle = (evt: MouseEvent | DraggableEvent) => {
|
||||||
const target = evt.target as HTMLElement;
|
const target = evt.target as HTMLElement;
|
||||||
|
|
||||||
@@ -65,17 +46,13 @@ const getHandleBounds = (
|
|||||||
const bounds = handle.getBoundingClientRect();
|
const bounds = handle.getBoundingClientRect();
|
||||||
const dimensions = getDimensions(handle);
|
const dimensions = getDimensions(handle);
|
||||||
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||||
const handlePosition = (handle.getAttribute(
|
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
|
||||||
'data-handlepos'
|
|
||||||
) as unknown) as Position;
|
|
||||||
const nodeIdSplitted = nodeIdAttr ? nodeIdAttr.split('__') : null;
|
const nodeIdSplitted = nodeIdAttr ? nodeIdAttr.split('__') : null;
|
||||||
|
|
||||||
let handleId = null;
|
let handleId = null;
|
||||||
|
|
||||||
if (nodeIdSplitted) {
|
if (nodeIdSplitted) {
|
||||||
handleId = (nodeIdSplitted.length
|
handleId = (nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted) as string;
|
||||||
? nodeIdSplitted[1]
|
|
||||||
: nodeIdSplitted) as string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -171,6 +148,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
onClick,
|
onClick,
|
||||||
onNodeDragStop,
|
onNodeDragStop,
|
||||||
style,
|
style,
|
||||||
|
isInteractive,
|
||||||
}: WrapNodeProps) => {
|
}: WrapNodeProps) => {
|
||||||
const nodeElement = useRef<HTMLDivElement>(null);
|
const nodeElement = useRef<HTMLDivElement>(null);
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||||
@@ -192,18 +170,8 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
const bounds = nodeElement.current.getBoundingClientRect();
|
const bounds = nodeElement.current.getBoundingClientRect();
|
||||||
const dimensions = getDimensions(nodeElement.current);
|
const dimensions = getDimensions(nodeElement.current);
|
||||||
const handleBounds = {
|
const handleBounds = {
|
||||||
source: getHandleBounds(
|
source: getHandleBounds('.source', nodeElement.current, bounds, storeState.transform[2]),
|
||||||
'.source',
|
target: getHandleBounds('.target', nodeElement.current, bounds, storeState.transform[2]),
|
||||||
nodeElement.current,
|
|
||||||
bounds,
|
|
||||||
storeState.transform[2]
|
|
||||||
),
|
|
||||||
target: getHandleBounds(
|
|
||||||
'.target',
|
|
||||||
nodeElement.current,
|
|
||||||
bounds,
|
|
||||||
storeState.transform[2]
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||||
};
|
};
|
||||||
@@ -232,43 +200,15 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggableCore
|
<DraggableCore
|
||||||
onStart={evt =>
|
onStart={evt => onStart(evt as MouseEvent, onClick, id, type, data, setOffset, transform, position)}
|
||||||
onStart(
|
onDrag={evt => onDrag(evt as MouseEvent, setDragging, id, offset, transform)}
|
||||||
evt as MouseEvent,
|
onStop={() => onStop(onNodeDragStop, isDragging, setDragging, id, type, position, data)}
|
||||||
onClick,
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
setOffset,
|
|
||||||
transform,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDrag={evt =>
|
|
||||||
onDrag(evt as MouseEvent, setDragging, id, offset, transform)
|
|
||||||
}
|
|
||||||
onStop={() =>
|
|
||||||
onStop(
|
|
||||||
onNodeDragStop,
|
|
||||||
isDragging,
|
|
||||||
setDragging,
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
position,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scale={transform[2]}
|
scale={transform[2]}
|
||||||
|
disabled={!isInteractive}
|
||||||
>
|
>
|
||||||
<div className={nodeClasses} ref={nodeElement} style={nodeStyle}>
|
<div className={nodeClasses} ref={nodeElement} style={nodeStyle}>
|
||||||
<Provider value={id}>
|
<Provider value={id}>
|
||||||
<NodeComponent
|
<NodeComponent id={id} data={data} type={type} style={style} selected={selected} />
|
||||||
id={id}
|
|
||||||
data={data}
|
|
||||||
type={type}
|
|
||||||
style={style}
|
|
||||||
selected={selected}
|
|
||||||
/>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
</DraggableCore>
|
</DraggableCore>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import React, { useEffect, useRef, useState, memo } from 'react';
|
|||||||
import { useStoreActions } from '../../store/hooks';
|
import { useStoreActions } from '../../store/hooks';
|
||||||
import { SelectionRect } from '../../types';
|
import { SelectionRect } from '../../types';
|
||||||
|
|
||||||
|
type UserSelectionProps = {
|
||||||
|
isInteractive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const initialRect: SelectionRect = {
|
const initialRect: SelectionRect = {
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
@@ -27,13 +31,17 @@ function getMousePosition(evt: MouseEvent) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(() => {
|
export default memo(({ isInteractive }: UserSelectionProps) => {
|
||||||
const selectionPane = useRef<HTMLDivElement>(null);
|
const selectionPane = useRef<HTMLDivElement>(null);
|
||||||
const [rect, setRect] = useState(initialRect);
|
const [rect, setRect] = useState(initialRect);
|
||||||
const setSelection = useStoreActions(a => a.setSelection);
|
const setSelection = useStoreActions(a => a.setSelection);
|
||||||
const updateSelection = useStoreActions(a => a.updateSelection);
|
const updateSelection = useStoreActions(a => a.updateSelection);
|
||||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||||
|
|
||||||
|
if (!isInteractive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onMouseDown(evt: MouseEvent): void {
|
function onMouseDown(evt: MouseEvent): void {
|
||||||
const mousePos = getMousePosition(evt);
|
const mousePos = getMousePosition(evt);
|
||||||
@@ -70,12 +78,8 @@ export default memo(() => {
|
|||||||
...currentRect,
|
...currentRect,
|
||||||
x: negativeX ? mousePos.x : currentRect.x,
|
x: negativeX ? mousePos.x : currentRect.x,
|
||||||
y: negativeY ? mousePos.y : currentRect.y,
|
y: negativeY ? mousePos.y : currentRect.y,
|
||||||
width: negativeX
|
width: negativeX ? currentRect.startX - mousePos.x : mousePos.x - currentRect.startX,
|
||||||
? currentRect.startX - mousePos.x
|
height: negativeY ? currentRect.startY - mousePos.y : mousePos.y - currentRect.startY,
|
||||||
: mousePos.x - currentRect.startX,
|
|
||||||
height: negativeY
|
|
||||||
? currentRect.startY - mousePos.y
|
|
||||||
: mousePos.y - currentRect.startY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSelection(nextRect);
|
updateSelection(nextRect);
|
||||||
|
|||||||
@@ -113,7 +113,13 @@ function getEdgePositions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEdge(edge: Edge, props: EdgeRendererProps, nodes: Node[], selectedElements: Elements) {
|
function renderEdge(
|
||||||
|
edge: Edge,
|
||||||
|
props: EdgeRendererProps,
|
||||||
|
nodes: Node[],
|
||||||
|
selectedElements: Elements,
|
||||||
|
isInteractive: boolean
|
||||||
|
) {
|
||||||
const [sourceId, sourceHandleId] = edge.source.split('__');
|
const [sourceId, sourceHandleId] = edge.source.split('__');
|
||||||
const [targetId, targetHandleId] = edge.target.split('__');
|
const [targetId, targetHandleId] = edge.target.split('__');
|
||||||
|
|
||||||
@@ -128,7 +134,7 @@ function renderEdge(edge: Edge, props: EdgeRendererProps, nodes: Node[], selecte
|
|||||||
throw new Error(`couldn't create edge for target id: ${targetId}`);
|
throw new Error(`couldn't create edge for target id: ${targetId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sourceNode.__rg.width || !sourceNode.__rg.height) {
|
if (!sourceNode.__rg.width || !sourceNode.__rg.height) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +177,7 @@ function renderEdge(edge: Edge, props: EdgeRendererProps, nodes: Node[], selecte
|
|||||||
targetY={targetY}
|
targetY={targetY}
|
||||||
sourcePosition={sourcePosition}
|
sourcePosition={sourcePosition}
|
||||||
targetPosition={targetPosition}
|
targetPosition={targetPosition}
|
||||||
|
isInteractive={isInteractive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -183,7 +190,16 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
|
|||||||
connectionSourceId,
|
connectionSourceId,
|
||||||
connectionPosition: { x, y },
|
connectionPosition: { x, y },
|
||||||
selectedElements,
|
selectedElements,
|
||||||
} = useStoreState(s => s);
|
isInteractive,
|
||||||
|
} = useStoreState(s => ({
|
||||||
|
transform: s.transform,
|
||||||
|
edges: s.edges,
|
||||||
|
nodes: s.nodes,
|
||||||
|
connectionSourceId: s.connectionSourceId,
|
||||||
|
connectionPosition: s.connectionPosition,
|
||||||
|
selectedElements: s.selectedElements,
|
||||||
|
isInteractive: s.isInteractive,
|
||||||
|
}));
|
||||||
|
|
||||||
const { width, height, connectionLineStyle, connectionLineType } = props;
|
const { width, height, connectionLineStyle, connectionLineType } = props;
|
||||||
|
|
||||||
@@ -197,7 +213,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
|
|||||||
return (
|
return (
|
||||||
<svg width={width} height={height} className="react-flow__edges">
|
<svg width={width} height={height} className="react-flow__edges">
|
||||||
<g transform={transformStyle}>
|
<g transform={transformStyle}>
|
||||||
{edges.map((e: Edge) => renderEdge(e, props, nodes, selectedElements))}
|
{edges.map((e: Edge) => renderEdge(e, props, nodes, selectedElements, isInteractive))}
|
||||||
{connectionSourceId && (
|
{connectionSourceId && (
|
||||||
<ConnectionLine
|
<ConnectionLine
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -207,6 +223,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
|
|||||||
transform={transform}
|
transform={transform}
|
||||||
connectionLineStyle={connectionLineStyle}
|
connectionLineStyle={connectionLineStyle}
|
||||||
connectionLineType={connectionLineType}
|
connectionLineType={connectionLineType}
|
||||||
|
isInteractive={isInteractive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, memo, SVGAttributes } from 'react';
|
import React, { useEffect, useRef, memo, SVGAttributes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||||
import NodeRenderer from '../NodeRenderer';
|
import NodeRenderer from '../NodeRenderer';
|
||||||
@@ -12,13 +13,7 @@ import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
|
|||||||
import useElementUpdater from '../../hooks/useElementUpdater';
|
import useElementUpdater from '../../hooks/useElementUpdater';
|
||||||
import { getDimensions } from '../../utils';
|
import { getDimensions } from '../../utils';
|
||||||
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
||||||
import {
|
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc } from '../../types';
|
||||||
Elements,
|
|
||||||
NodeTypesType,
|
|
||||||
EdgeTypesType,
|
|
||||||
GridType,
|
|
||||||
OnLoadFunc,
|
|
||||||
} from '../../types';
|
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
elements: Elements;
|
elements: Elements;
|
||||||
@@ -40,7 +35,8 @@ export interface GraphViewProps {
|
|||||||
backgroundType: GridType;
|
backgroundType: GridType;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean
|
onlyRenderVisibleNodes: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphView = memo(
|
const GraphView = memo(
|
||||||
@@ -64,7 +60,8 @@ const GraphView = memo(
|
|||||||
onConnect,
|
onConnect,
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
snapGrid,
|
snapGrid,
|
||||||
onlyRenderVisibleNodes
|
onlyRenderVisibleNodes,
|
||||||
|
isInteractive,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
const zoomPane = useRef<HTMLDivElement>(null);
|
const zoomPane = useRef<HTMLDivElement>(null);
|
||||||
const rendererNode = useRef<HTMLDivElement>(null);
|
const rendererNode = useRef<HTMLDivElement>(null);
|
||||||
@@ -77,13 +74,12 @@ const GraphView = memo(
|
|||||||
nodesSelectionActive: s.nodesSelectionActive,
|
nodesSelectionActive: s.nodesSelectionActive,
|
||||||
}));
|
}));
|
||||||
const updateSize = useStoreActions(actions => actions.updateSize);
|
const updateSize = useStoreActions(actions => actions.updateSize);
|
||||||
const setNodesSelection = useStoreActions(
|
const setNodesSelection = useStoreActions(actions => actions.setNodesSelection);
|
||||||
actions => actions.setNodesSelection
|
|
||||||
);
|
|
||||||
const setOnConnect = useStoreActions(a => a.setOnConnect);
|
const setOnConnect = useStoreActions(a => a.setOnConnect);
|
||||||
const setSnapGrid = useStoreActions(actions => actions.setSnapGrid);
|
const setSnapGrid = useStoreActions(actions => actions.setSnapGrid);
|
||||||
|
const setInteractive = useStoreActions(actions => actions.setInteractive);
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||||
|
const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });
|
||||||
|
|
||||||
const onZoomPaneClick = () => setNodesSelection({ isActive: false });
|
const onZoomPaneClick = () => setNodesSelection({ isActive: false });
|
||||||
|
|
||||||
@@ -122,17 +118,17 @@ const GraphView = memo(
|
|||||||
setSnapGrid({ snapToGrid, snapGrid });
|
setSnapGrid({ snapToGrid, snapGrid });
|
||||||
}, [snapToGrid]);
|
}, [snapToGrid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInteractive(isInteractive);
|
||||||
|
}, [isInteractive]);
|
||||||
|
|
||||||
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
||||||
useElementUpdater(elements);
|
useElementUpdater(elements);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__renderer" ref={rendererNode}>
|
<div className={rendererClasses} ref={rendererNode}>
|
||||||
{showBackground && (
|
{showBackground && (
|
||||||
<BackgroundGrid
|
<BackgroundGrid gap={backgroundGap} color={backgroundColor} backgroundType={backgroundType} />
|
||||||
gap={backgroundGap}
|
|
||||||
color={backgroundColor}
|
|
||||||
backgroundType={backgroundType}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<NodeRenderer
|
<NodeRenderer
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
@@ -148,13 +144,9 @@ const GraphView = memo(
|
|||||||
connectionLineType={connectionLineType}
|
connectionLineType={connectionLineType}
|
||||||
connectionLineStyle={connectionLineStyle}
|
connectionLineStyle={connectionLineStyle}
|
||||||
/>
|
/>
|
||||||
{selectionKeyPressed && <UserSelection />}
|
{selectionKeyPressed && <UserSelection isInteractive={isInteractive} />}
|
||||||
{state.nodesSelectionActive && <NodesSelection />}
|
{state.nodesSelectionActive && <NodesSelection />}
|
||||||
<div
|
<div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} />
|
||||||
className="react-flow__zoompane"
|
|
||||||
onClick={onZoomPaneClick}
|
|
||||||
ref={zoomPane}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { memo, ComponentType } from 'react';
|
|||||||
|
|
||||||
import { useStoreState } from '../../store/hooks';
|
import { useStoreState } from '../../store/hooks';
|
||||||
import { getNodesInside } from '../../utils/graph';
|
import { getNodesInside } from '../../utils/graph';
|
||||||
import { Node, Transform, NodeTypesType, NodeComponentProps, Elements } from '../../types';
|
import { Node, Transform, NodeTypesType, WrapNodeProps, Elements } from '../../types';
|
||||||
|
|
||||||
interface NodeRendererProps {
|
interface NodeRendererProps {
|
||||||
nodeTypes: NodeTypesType;
|
nodeTypes: NodeTypesType;
|
||||||
@@ -11,9 +11,15 @@ interface NodeRendererProps {
|
|||||||
onlyRenderVisibleNodes?: boolean;
|
onlyRenderVisibleNodes?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNode(node: Node, props: NodeRendererProps, transform: Transform, selectedElements: Elements) {
|
function renderNode(
|
||||||
|
node: Node,
|
||||||
|
props: NodeRendererProps,
|
||||||
|
transform: Transform,
|
||||||
|
selectedElements: Elements,
|
||||||
|
isInteractive: boolean
|
||||||
|
) {
|
||||||
const nodeType = node.type || 'default';
|
const nodeType = node.type || 'default';
|
||||||
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<NodeComponentProps>;
|
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
||||||
if (!props.nodeTypes[nodeType]) {
|
if (!props.nodeTypes[nodeType]) {
|
||||||
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
|
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
|
||||||
}
|
}
|
||||||
@@ -33,23 +39,33 @@ function renderNode(node: Node, props: NodeRendererProps, transform: Transform,
|
|||||||
transform={transform}
|
transform={transform}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
style={node.style}
|
style={node.style}
|
||||||
|
isInteractive={isInteractive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeRenderer = memo(({ onlyRenderVisibleNodes = true, ...props }: NodeRendererProps) => {
|
const NodeRenderer = memo(({ onlyRenderVisibleNodes = true, ...props }: NodeRendererProps) => {
|
||||||
const { nodes, transform, selectedElements, width, height } = useStoreState(s => s);
|
const { nodes, transform, selectedElements, width, height, isInteractive } = useStoreState(s => ({
|
||||||
|
nodes: s.nodes,
|
||||||
|
transform: s.transform,
|
||||||
|
selectedElements: s.selectedElements,
|
||||||
|
width: s.width,
|
||||||
|
height: s.height,
|
||||||
|
isInteractive: s.isInteractive,
|
||||||
|
}));
|
||||||
|
|
||||||
const [tx, ty, tScale] = transform;
|
const [tx, ty, tScale] = transform;
|
||||||
const transformStyle = {
|
const transformStyle = {
|
||||||
transform: `translate(${tx}px,${ty}px) scale(${tScale})`,
|
transform: `translate(${tx}px,${ty}px) scale(${tScale})`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderNodes = onlyRenderVisibleNodes ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) : nodes;
|
const renderNodes = onlyRenderVisibleNodes
|
||||||
|
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
|
||||||
|
: nodes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__nodes" style={transformStyle}>
|
<div className="react-flow__nodes" style={transformStyle}>
|
||||||
{renderNodes.map(node => renderNode(node, props, transform, selectedElements))}
|
{renderNodes.map(node => renderNode(node, props, transform, selectedElements, isInteractive))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,18 +18,11 @@ import StraightEdge from '../../components/Edges/StraightEdge';
|
|||||||
import StepEdge from '../../components/Edges/StepEdge';
|
import StepEdge from '../../components/Edges/StepEdge';
|
||||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import {
|
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc } from '../../types';
|
||||||
Elements,
|
|
||||||
NodeTypesType,
|
|
||||||
EdgeTypesType,
|
|
||||||
GridType,
|
|
||||||
OnLoadFunc,
|
|
||||||
} from '../../types';
|
|
||||||
|
|
||||||
import '../../style.css';
|
import '../../style.css';
|
||||||
|
|
||||||
export interface ReactFlowProps
|
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
|
||||||
extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
|
|
||||||
elements: Elements;
|
elements: Elements;
|
||||||
onElementClick: () => void;
|
onElementClick: () => void;
|
||||||
onElementsRemove: (elements: Elements) => void;
|
onElementsRemove: (elements: Elements) => void;
|
||||||
@@ -49,7 +42,8 @@ export interface ReactFlowProps
|
|||||||
backgroundType: GridType;
|
backgroundType: GridType;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean
|
onlyRenderVisibleNodes: boolean;
|
||||||
|
isInteractive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactFlow = ({
|
const ReactFlow = ({
|
||||||
@@ -74,7 +68,8 @@ const ReactFlow = ({
|
|||||||
backgroundColor,
|
backgroundColor,
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
snapGrid,
|
snapGrid,
|
||||||
onlyRenderVisibleNodes
|
onlyRenderVisibleNodes,
|
||||||
|
isInteractive,
|
||||||
}: ReactFlowProps) => {
|
}: ReactFlowProps) => {
|
||||||
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
||||||
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
|
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
|
||||||
@@ -103,6 +98,7 @@ const ReactFlow = ({
|
|||||||
snapToGrid={snapToGrid}
|
snapToGrid={snapToGrid}
|
||||||
snapGrid={snapGrid}
|
snapGrid={snapGrid}
|
||||||
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
|
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
|
||||||
|
isInteractive={isInteractive}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
@@ -139,7 +135,8 @@ ReactFlow.defaultProps = {
|
|||||||
backgroundType: GridType.Dots,
|
backgroundType: GridType.Dots,
|
||||||
snapToGrid: false,
|
snapToGrid: false,
|
||||||
snapGrid: [16, 16],
|
snapGrid: [16, 16],
|
||||||
onlyRenderVisibleNodes: true
|
onlyRenderVisibleNodes: true,
|
||||||
|
isInteractive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReactFlow;
|
export default ReactFlow;
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ const d3ZoomInstance = d3Zoom
|
|||||||
.scaleExtent([0.5, 2])
|
.scaleExtent([0.5, 2])
|
||||||
.filter(() => !event.button);
|
.filter(() => !event.button);
|
||||||
|
|
||||||
export default (
|
export default (zoomPane: MutableRefObject<Element | null>, onMove: () => void, shiftPressed: boolean): void => {
|
||||||
zoomPane: MutableRefObject<Element | null>,
|
|
||||||
onMove: () => void,
|
|
||||||
shiftPressed: boolean
|
|
||||||
): void => {
|
|
||||||
const state = useStoreState(s => ({
|
const state = useStoreState(s => ({
|
||||||
transform: s.transform,
|
transform: s.transform,
|
||||||
d3Selection: s.d3Selection,
|
d3Selection: s.d3Selection,
|
||||||
@@ -35,10 +31,7 @@ export default (
|
|||||||
d3ZoomInstance.on('zoom', null);
|
d3ZoomInstance.on('zoom', null);
|
||||||
} else {
|
} else {
|
||||||
d3ZoomInstance.on('zoom', () => {
|
d3ZoomInstance.on('zoom', () => {
|
||||||
if (
|
if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) {
|
||||||
event.sourceEvent &&
|
|
||||||
event.sourceEvent.target !== zoomPane.current
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-29
@@ -2,11 +2,7 @@ import { createStore, Action, action } 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 {
|
import { getNodesInside, getConnectedEdges, getRectOfNodes } from '../utils/graph';
|
||||||
getNodesInside,
|
|
||||||
getConnectedEdges,
|
|
||||||
getRectOfNodes,
|
|
||||||
} from '../utils/graph';
|
|
||||||
import {
|
import {
|
||||||
ElementId,
|
ElementId,
|
||||||
Elements,
|
Elements,
|
||||||
@@ -80,6 +76,8 @@ export interface StoreModel {
|
|||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
|
|
||||||
|
isInteractive: boolean;
|
||||||
|
|
||||||
onConnect: OnConnectFunc;
|
onConnect: OnConnectFunc;
|
||||||
|
|
||||||
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
||||||
@@ -111,6 +109,8 @@ export interface StoreModel {
|
|||||||
setConnectionPosition: Action<StoreModel, XYPosition>;
|
setConnectionPosition: Action<StoreModel, XYPosition>;
|
||||||
|
|
||||||
setConnectionSourceId: Action<StoreModel, ElementId | null>;
|
setConnectionSourceId: Action<StoreModel, ElementId | null>;
|
||||||
|
|
||||||
|
setInteractive: Action<StoreModel, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeModel: StoreModel = {
|
const storeModel: StoreModel = {
|
||||||
@@ -136,6 +136,8 @@ const storeModel: StoreModel = {
|
|||||||
snapGrid: [16, 16],
|
snapGrid: [16, 16],
|
||||||
snapToGrid: true,
|
snapToGrid: true,
|
||||||
|
|
||||||
|
isInteractive: true,
|
||||||
|
|
||||||
onConnect: () => {},
|
onConnect: () => {},
|
||||||
|
|
||||||
setOnConnect: action((state, onConnect) => {
|
setOnConnect: action((state, onConnect) => {
|
||||||
@@ -195,11 +197,7 @@ const storeModel: StoreModel = {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedNodes = getNodesInside(
|
const selectedNodes = getNodesInside(state.nodes, selection, state.transform);
|
||||||
state.nodes,
|
|
||||||
selection,
|
|
||||||
state.transform
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedNodes.length) {
|
if (!selectedNodes.length) {
|
||||||
state.nodesSelectionActive = false;
|
state.nodesSelectionActive = false;
|
||||||
@@ -218,35 +216,21 @@ const storeModel: StoreModel = {
|
|||||||
|
|
||||||
setSelectedElements: action((state, elements) => {
|
setSelectedElements: action((state, elements) => {
|
||||||
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
||||||
const selectedElementsUpdated = !isEqual(
|
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
|
||||||
selectedElementsArr,
|
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
|
||||||
state.selectedElements
|
|
||||||
);
|
|
||||||
const selectedElements = selectedElementsUpdated
|
|
||||||
? selectedElementsArr
|
|
||||||
: state.selectedElements;
|
|
||||||
|
|
||||||
state.selectedElements = selectedElements;
|
state.selectedElements = selectedElements;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSelection: action((state, selection) => {
|
updateSelection: action((state, selection) => {
|
||||||
const selectedNodes = getNodesInside(
|
const selectedNodes = getNodesInside(state.nodes, selection, state.transform);
|
||||||
state.nodes,
|
|
||||||
selection,
|
|
||||||
state.transform
|
|
||||||
);
|
|
||||||
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
||||||
|
|
||||||
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
||||||
const selectedElementsUpdated = !isEqual(
|
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
||||||
nextSelectedElements,
|
|
||||||
state.selectedElements
|
|
||||||
);
|
|
||||||
|
|
||||||
state.selection = selection;
|
state.selection = selection;
|
||||||
state.selectedElements = selectedElementsUpdated
|
state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements;
|
||||||
? nextSelectedElements
|
|
||||||
: state.selectedElements;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateTransform: action((state, transform) => {
|
updateTransform: action((state, transform) => {
|
||||||
@@ -276,6 +260,10 @@ const storeModel: StoreModel = {
|
|||||||
state.snapToGrid = snapToGrid;
|
state.snapToGrid = snapToGrid;
|
||||||
state.snapGrid = snapGrid;
|
state.snapGrid = snapGrid;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setInteractive: action((state, isInteractive) => {
|
||||||
|
state.isInteractive = isInteractive;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = createStore(storeModel);
|
const store = createStore(storeModel);
|
||||||
|
|||||||
+17
-8
@@ -67,7 +67,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes dashdraw {
|
@keyframes dashdraw {
|
||||||
from {stroke-dashoffset: 10}
|
from {
|
||||||
|
stroke-dashoffset: 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__nodes {
|
.react-flow__nodes {
|
||||||
@@ -79,21 +81,30 @@
|
|||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-interactive {
|
||||||
|
.react-flow__node {
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
&:hover > * {
|
||||||
|
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.react-flow__node {
|
.react-flow__node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: #222;
|
color: #222;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: grab;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
|
||||||
&:hover > * {
|
|
||||||
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected > * {
|
&.selected > * {
|
||||||
box-shadow: 0 0 0 2px #555;
|
box-shadow: 0 0 0 2px #555;
|
||||||
}
|
}
|
||||||
@@ -104,7 +115,6 @@
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: rgba(255, 255, 255, 0.4);
|
background: rgba(255, 255, 255, 0.4);
|
||||||
cursor: crosshair;
|
|
||||||
|
|
||||||
&.bottom {
|
&.bottom {
|
||||||
top: auto;
|
top: auto;
|
||||||
@@ -123,7 +133,6 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0;
|
left: 0;
|
||||||
transform: translate(0, -50%);
|
transform: translate(0, -50%);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
|
|||||||
@@ -99,6 +99,20 @@ export interface NodeComponentProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WrapNodeProps {
|
||||||
|
id: ElementId;
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
selected: boolean;
|
||||||
|
transform: Transform;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
isInteractive: boolean;
|
||||||
|
onClick: (node: Node) => void | undefined;
|
||||||
|
onNodeDragStop: (node: Node) => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
export type FitViewParams = {
|
export type FitViewParams = {
|
||||||
padding: number;
|
padding: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user