Feature: Draggable edge
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -228,3 +228,7 @@
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.move-handler {
|
||||
cursor: move;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user