Feature: Draggable edge

This commit is contained in:
Long Nguyen
2020-10-12 21:34:35 +09:00
parent f27634ebe2
commit bd63a0131a
13 changed files with 6583 additions and 9 deletions
+7
View File
@@ -105,6 +105,7 @@ const BasicFlow = () => <ReactFlow elements={elements} />;
- `onPaneClick(event: MouseEvent)`: called when user clicks directly on the canvas
- `onPaneContextMenu(event: MouseEvent)`: called when user does a right-click on the canvas
- `onPaneScroll(event: WheelEvent)`: called when user scrolls pane (only works when `zoomOnScroll` is set to `false)
- `onEdgeUpdate(oldEdge: Edge, newConnection: Connection)`: called when user press at either end of the edge and point to the target node
#### Interaction
- `nodesDraggable`: default: `true`. This applies to all nodes. You can also change the behavior of a specific node with the `draggable` node option
@@ -586,6 +587,12 @@ Returns an array with elements with the added edge.
`addEdge = (edgeParams: Edge, elements: Elements): Elements`
### updateEdge
Returns an array with elements with the updated edge.
`updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements`
### getOutgoers
Returns all direct child nodes of the passed node.
+87
View File
@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import ReactFlow, { MiniMap, Controls, Background, updateEdge } from 'react-flow-renderer';
const onLoad = (reactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView();
};
const initialElements = [
{
id: '1',
type: 'input',
data: {
label: (
<>
Node <strong>A</strong>
</>
),
},
position: { x: 250, y: 0 },
},
{
id: '2',
data: {
label: (
<>
Node <strong>B</strong>
</>
),
},
position: { x: 100, y: 100 },
},
{
id: '3',
data: {
label: (
<>
Node <strong>C</strong>
</>
),
},
position: { x: 400, y: 100 },
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
},
{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' },
];
const snapGrid = [16, 16];
const DragEdge = () => {
const [elements, setElements] = useState(initialElements);
const onEdgeUpdate = (oldEdge, newConnection) => {
setElements((els) => updateEdge(oldEdge, newConnection, els));
}
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
snapToGrid={true}
snapGrid={snapGrid}
onEdgeUpdate={onEdgeUpdate}
>
<MiniMap
nodeStrokeColor={(n) => {
if (n.style?.background) return n.style.background;
if (n.type === 'input') return '#0041d0';
if (n.type === 'output') return '#ff0072';
if (n.type === 'default') return '#1a192b';
return '#eee';
}}
nodeColor={(n) => {
if (n.style?.background) return n.style.background;
return '#fff';
}}
borderRadius={2}
/>
<Controls />
<Background color="#aaa" gap={16} />
</ReactFlow>
);
};
export default DragEdge;
+6
View File
@@ -16,6 +16,7 @@ import Hidden from './Hidden';
import EdgeTypes from './EdgeTypes';
import CustomConnectionLine from './CustomConnectionLine';
import NodeTypeChange from './NodeTypeChange';
import DraggableEdge from './DraggableEdge';
import './index.css';
@@ -84,6 +85,11 @@ const routes = [
path: '/nodetype-change',
component: NodeTypeChange,
},
{
path: '/draggable-edge',
component: DraggableEdge,
label: 'Draggable Edge',
},
];
const navLinks = routes.filter((route) => route.label);
+52
View File
@@ -32,6 +32,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
elementsSelectable,
markerEndId,
isHidden,
onEitherEndOfEdgePress,
}: WrapEdgeProps) => {
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements);
@@ -63,12 +64,53 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
[elementsSelectable, id, source, target, type, data, onClick]
);
const handleEitherEndOfEdgePress = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>, isEdgeHeader?: boolean): void => {
if (elementsSelectable) {
setSelectedElements({ id, source, target });
}
const edgeElement: Edge = { id, source, target, type };
if (typeof data !== 'undefined') {
edgeElement.data = data;
}
onEitherEndOfEdgePress(event, edgeElement, isEdgeHeader);
},
[elementsSelectable, id, source, target, type, data, onEitherEndOfEdgePress]
);
const handleEdgeHeaderPress = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
handleEitherEndOfEdgePress(event, true);
},
[handleEitherEndOfEdgePress],
);
const handleEdgeFooterPress = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
handleEitherEndOfEdgePress(event);
},
[handleEitherEndOfEdgePress],
);
if (isHidden) {
return null;
}
return (
<g className={edgeClasses} onClick={onEdgeClick} style={edgeGroupStyle}>
<g onMouseDown={handleEdgeFooterPress}>
<circle
className="move-handler"
cx={sourceX}
cy={sourceY}
r="12"
stroke="transparent"
fill="transparent"
/>
</g>
<EdgeComponent
id={id}
source={source}
@@ -92,6 +134,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
targetPosition={targetPosition}
markerEndId={markerEndId}
/>
<g onMouseDown={handleEdgeHeaderPress}>
<circle
className="move-handler"
cx={targetX}
cy={targetY}
r="12"
stroke="transparent"
fill="transparent"
/>
</g>
</g>
);
};
+4 -4
View File
@@ -15,7 +15,7 @@ import {
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
interface BaseHandleProps {
type: HandleType;
@@ -40,7 +40,7 @@ type Result = {
isHoveringHandle: boolean;
};
function onMouseDown(
export function onMouseDown(
event: ReactMouseEvent,
nodeId: ElementId,
setConnectionNodeId: SetSourceIdFunc,
@@ -67,7 +67,7 @@ function onMouseDown(
y: event.clientY - containerBounds.top,
});
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleType: handleType });
if (onConnectStart) {
onConnectStart(event, { nodeId, handleType });
}
@@ -136,7 +136,7 @@ function onMouseDown(
function onMouseUp(event: MouseEvent) {
const { connection, isValid } = checkElementBelowIsValid(event);
if (onConnectStop) {
onConnectStop(event);
}
+45 -3
View File
@@ -1,6 +1,6 @@
import React, { memo, CSSProperties } from 'react';
import { useStoreState } from '../../store/hooks';
import { useStoreState, useStoreActions } from '../../store/hooks';
import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions';
@@ -14,7 +14,10 @@ import {
Elements,
ConnectionLineType,
ConnectionLineComponent,
Connection,
OnEdgeUpdateFunc
} from '../../types';
import { onMouseDown, SetSourceIdFunc } from '../../components/Handle/BaseHandle';
interface EdgeRendererProps {
edgeTypes: any;
@@ -24,6 +27,7 @@ interface EdgeRendererProps {
arrowHeadColor: string;
markerEndId?: string;
connectionLineComponent?: ConnectionLineComponent;
onEdgeUpdate?: OnEdgeUpdateFunc;
}
interface EdgePositions {
@@ -130,7 +134,9 @@ function renderEdge(
props: EdgeRendererProps,
nodes: Node[],
selectedElements: Elements | null,
elementsSelectable: boolean
elementsSelectable: boolean,
setConnectionNodeId: SetSourceIdFunc,
setPosition: (pos: XYPosition) => void,
) {
const [sourceId, sourceHandleId] = edge.source.split('__');
const [targetId, targetHandleId] = edge.target.split('__');
@@ -168,6 +174,30 @@ function renderEdge(
const isSelected = selectedElements ? selectedElements.some((elm) => isEdge(elm) && elm.id === edge.id) : false;
const onConnect = (connection: Connection) => {
const { onEdgeUpdate } = props;
if (onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
}
const handleEitherEndOfEdgePress = (event: React.MouseEvent, edge: Edge, isEdgeHeader = false) => {
const { source, target } = edge;
const nodeId = isEdgeHeader ? source : target;
const isValidConnection = () => true;
const isTarget = !isEdgeHeader;
onMouseDown(
event,
nodeId,
setConnectionNodeId,
setPosition,
onConnect,
isTarget,
isValidConnection,
)
}
return (
<EdgeComponent
key={edge.id}
@@ -199,6 +229,7 @@ function renderEdge(
elementsSelectable={elementsSelectable}
markerEndId={props.markerEndId}
isHidden={edge.isHidden}
onEitherEndOfEdgePress={handleEitherEndOfEdgePress}
/>
);
}
@@ -215,6 +246,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props;
@@ -229,7 +262,16 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} />
<g transform={transformStyle}>
{edges.map((edge: Edge) => renderEdge(edge, props, nodes, selectedElements, elementsSelectable))}
{edges.map((edge: Edge) =>
renderEdge(
edge,
props,
nodes,
selectedElements,
elementsSelectable,
setConnectionNodeId,
setPosition,
))}
{renderConnectionLine && (
<ConnectionLine
nodes={nodes}
+5 -1
View File
@@ -25,7 +25,8 @@ import {
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
TranslateExtent,
TranslateExtent,
OnEdgeUpdateFunc,
} from '../../types';
export interface GraphViewProps {
@@ -77,6 +78,7 @@ export interface GraphViewProps {
zoomOnScroll?: boolean;
zoomOnDoubleClick?: boolean;
paneMoveable?: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
}
const GraphView = ({
@@ -128,6 +130,7 @@ const GraphView = ({
onPaneClick,
onPaneScroll,
onPaneContextMenu,
onEdgeUpdate,
}: GraphViewProps) => {
const isInitialised = useRef<boolean>(false);
const zoomPane = useRef<HTMLDivElement>(null);
@@ -310,6 +313,7 @@ const GraphView = ({
arrowHeadColor={arrowHeadColor}
markerEndId={markerEndId}
connectionLineComponent={connectionLineComponent}
onEdgeUpdate={onEdgeUpdate}
/>
<UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && (
+4
View File
@@ -32,6 +32,7 @@ import {
OnConnectStopFunc,
OnConnectEndFunc,
TranslateExtent,
OnEdgeUpdateFunc,
} from '../../types';
import '../../style.css';
@@ -99,6 +100,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
markerEndId?: string;
zoomOnScroll?: boolean;
zoomOnDoubleClick?: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
}
const ReactFlow = ({
@@ -153,6 +155,7 @@ const ReactFlow = ({
onPaneScroll,
onPaneContextMenu,
children,
onEdgeUpdate,
...rest
}: ReactFlowProps) => {
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
@@ -211,6 +214,7 @@ const ReactFlow = ({
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
onEdgeUpdate={onEdgeUpdate}
/>
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
+1 -1
View File
@@ -8,7 +8,7 @@ export { getBezierPath } from './components/Edges/BezierEdge';
export { getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph';
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges, updateEdge } from './utils/graph';
export * from './additional-components';
export * from './types';
+4
View File
@@ -228,3 +228,7 @@
top: 50%;
transform: translate(0, -50%);
}
.move-handler {
cursor: move;
}
+3
View File
@@ -119,6 +119,7 @@ export interface WrapEdgeProps {
elementsSelectable?: boolean;
markerEndId?: string;
isHidden?: boolean;
onEitherEndOfEdgePress: (event: React.MouseEvent, edge: Edge, isEdgeHeader?: boolean) => void;
}
export interface EdgeProps {
@@ -311,3 +312,5 @@ export type FlowTransform = {
};
export type TranslateExtent = [[number, number], [number, number]];
export type OnEdgeUpdateFunc = (oldEdge: Edge, newConnection: Connection) => void;
+31
View File
@@ -67,6 +67,37 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
return elements.concat(edge);
};
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => {
if (!newConnection.source || !newConnection.target) {
throw new Error("Can't create new edge. An edge needs a source and a target.");
}
// make sure that there is node with the target and one with the source id
[newConnection.source, newConnection.target].forEach((id) => {
const nodeId = id.includes('__') ? id.split('__')[0] : id;
if (!elements.find((e) => isNode(e) && e.id === nodeId)) {
throw new Error(`Can't create edge. Node with id=${nodeId} does not exist.`);
}
});
const foundEdge = elements.find(e => isEdge(e) && e.id === oldEdge.id) as Edge;
if (!foundEdge) {
throw new Error(`The old edge with id=${oldEdge.id} does not exist.`);
}
// Remove old edge and create the new edge with parameters of old edge.
const edge = {
...oldEdge,
id: getEdgeId(newConnection),
source: newConnection.source,
target: newConnection.target,
} as Edge;
return elements.filter(e => e.id !== oldEdge.id).concat(edge);
};
export const pointToRendererPoint = (
{ x, y }: XYPosition,
[tx, ty, tScale]: Transform,
+6334
View File
File diff suppressed because it is too large Load Diff