Feature: Draggable edge
This commit is contained in:
@@ -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