@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { MiniMap, Controls, Background, updateEdge, addEdge } 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));
|
||||
};
|
||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
snapToGrid={true}
|
||||
snapGrid={snapGrid}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<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;
|
||||
@@ -15,6 +15,7 @@ import Hidden from './Hidden';
|
||||
import EdgeTypes from './EdgeTypes';
|
||||
import CustomConnectionLine from './CustomConnectionLine';
|
||||
import NodeTypeChange from './NodeTypeChange';
|
||||
import DraggableEdge from './DraggableEdge';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -78,6 +79,11 @@ const routes = [
|
||||
path: '/nodetype-change',
|
||||
component: NodeTypeChange,
|
||||
},
|
||||
{
|
||||
path: '/draggable-edge',
|
||||
component: DraggableEdge,
|
||||
label: 'Draggable Edge',
|
||||
},
|
||||
];
|
||||
|
||||
const navLinks = routes.filter((route) => route.label);
|
||||
|
||||
Generated
+8424
-11
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import React, { memo, ComponentType, useCallback } from 'react';
|
||||
import React, { memo, ComponentType, useCallback, useState } from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStoreActions } from '../../store/hooks';
|
||||
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
|
||||
import { onMouseDown } from '../../components/Handle/BaseHandle';
|
||||
|
||||
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const EdgeWrapper = ({
|
||||
@@ -32,15 +33,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
elementsSelectable,
|
||||
markerEndId,
|
||||
isHidden,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
handleEdgeUpdate,
|
||||
onConnectEdge,
|
||||
}: WrapEdgeProps) => {
|
||||
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
|
||||
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
|
||||
const inactive = !elementsSelectable && !onClick;
|
||||
const edgeClasses = cc([
|
||||
'react-flow__edge',
|
||||
`react-flow__edge-${type}`,
|
||||
className,
|
||||
{ selected, animated, inactive },
|
||||
{ selected, animated, inactive, updating },
|
||||
]);
|
||||
|
||||
const onEdgeClick = useCallback(
|
||||
@@ -62,12 +70,66 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
[elementsSelectable, id, source, target, type, data, onClick]
|
||||
);
|
||||
|
||||
const handleEdgeUpdater = useCallback(
|
||||
(event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
|
||||
const nodeId = isSourceHandle ? target : source;
|
||||
const handleId = isSourceHandle ? targetHandleId : sourceHandleId;
|
||||
const isValidConnection = () => true;
|
||||
const isTarget = isSourceHandle;
|
||||
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
onConnectEdge,
|
||||
isTarget,
|
||||
isValidConnection
|
||||
);
|
||||
},
|
||||
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition]
|
||||
);
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = useCallback(
|
||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
handleEdgeUpdater(event, true);
|
||||
},
|
||||
[id, source, sourceHandleId, handleEdgeUpdater]
|
||||
);
|
||||
|
||||
const onEdgeUpdaterTargetMouseDown = useCallback(
|
||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
handleEdgeUpdater(event, false);
|
||||
},
|
||||
[id, target, targetHandleId, handleEdgeUpdater]
|
||||
);
|
||||
|
||||
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
|
||||
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
|
||||
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<g className={edgeClasses} onClick={onEdgeClick}>
|
||||
{handleEdgeUpdate && (
|
||||
<g
|
||||
onMouseDown={onEdgeUpdaterSourceMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
>
|
||||
<circle
|
||||
className="react-flow__edgeupdater"
|
||||
cx={sourceX}
|
||||
cy={sourceY}
|
||||
r={10}
|
||||
stroke="transparent"
|
||||
fill="transparent"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
<EdgeComponent
|
||||
id={id}
|
||||
source={source}
|
||||
@@ -91,6 +153,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
targetPosition={targetPosition}
|
||||
markerEndId={markerEndId}
|
||||
/>
|
||||
{handleEdgeUpdate && (
|
||||
<g
|
||||
onMouseDown={onEdgeUpdaterTargetMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
>
|
||||
<circle
|
||||
className="react-flow__edgeupdater"
|
||||
cx={targetX}
|
||||
cy={targetY}
|
||||
r={10}
|
||||
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,
|
||||
handleId: ElementId | null,
|
||||
nodeId: ElementId,
|
||||
@@ -67,6 +67,7 @@ function onMouseDown(
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
});
|
||||
|
||||
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
|
||||
|
||||
if (onConnectStart) {
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import React, { memo, CSSProperties } from 'react';
|
||||
import React, { memo, CSSProperties, useCallback } from 'react';
|
||||
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import ConnectionLine from '../../components/ConnectionLine/index';
|
||||
import { isEdge } from '../../utils/graph';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils';
|
||||
import { Position, Edge, Node, Elements, ConnectionLineType, ConnectionLineComponent, Transform } from '../../types';
|
||||
import {
|
||||
Position,
|
||||
Edge,
|
||||
Node,
|
||||
Elements,
|
||||
ConnectionLineType,
|
||||
ConnectionLineComponent,
|
||||
Transform,
|
||||
OnEdgeUpdateFunc,
|
||||
Connection,
|
||||
} from '../../types';
|
||||
|
||||
interface EdgeRendererProps {
|
||||
edgeTypes: any;
|
||||
@@ -16,23 +26,43 @@ interface EdgeRendererProps {
|
||||
markerEndId?: string;
|
||||
connectionLineComponent?: ConnectionLineComponent;
|
||||
onlyRenderVisibleElements: boolean;
|
||||
onEdgeUpdate?: OnEdgeUpdateFunc;
|
||||
}
|
||||
|
||||
function renderEdge(
|
||||
edge: Edge,
|
||||
props: EdgeRendererProps,
|
||||
nodes: Node[],
|
||||
selectedElements: Elements | null,
|
||||
elementsSelectable: boolean,
|
||||
transform: Transform,
|
||||
width: number,
|
||||
height: number,
|
||||
onlyRenderVisibleElements: boolean
|
||||
) {
|
||||
interface EdgeWrapperProps {
|
||||
edge: Edge;
|
||||
props: EdgeRendererProps;
|
||||
nodes: Node[];
|
||||
selectedElements: Elements | null;
|
||||
elementsSelectable: boolean;
|
||||
transform: Transform;
|
||||
width: number;
|
||||
height: number;
|
||||
onlyRenderVisibleElements: boolean;
|
||||
}
|
||||
|
||||
const Edge = ({
|
||||
edge,
|
||||
props,
|
||||
nodes,
|
||||
selectedElements,
|
||||
elementsSelectable,
|
||||
transform,
|
||||
width,
|
||||
height,
|
||||
onlyRenderVisibleElements,
|
||||
}: EdgeWrapperProps) => {
|
||||
const sourceHandleId = edge.sourceHandle || null;
|
||||
const targetHandleId = edge.targetHandle || null;
|
||||
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
|
||||
|
||||
const onConnectEdge = useCallback(
|
||||
(connection: Connection) => {
|
||||
props.onEdgeUpdate?.(edge, connection);
|
||||
},
|
||||
[edge]
|
||||
);
|
||||
|
||||
if (!sourceNode) {
|
||||
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
|
||||
return null;
|
||||
@@ -120,9 +150,11 @@ function renderEdge(
|
||||
elementsSelectable={elementsSelectable}
|
||||
markerEndId={props.markerEndId}
|
||||
isHidden={edge.isHidden}
|
||||
onConnectEdge={onConnectEdge}
|
||||
handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const EdgeRenderer = (props: EdgeRendererProps) => {
|
||||
const transform = useStoreState((state) => state.transform);
|
||||
@@ -156,19 +188,20 @@ 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,
|
||||
transform,
|
||||
width,
|
||||
height,
|
||||
onlyRenderVisibleElements
|
||||
)
|
||||
)}
|
||||
{edges.map((edge: Edge) => (
|
||||
<Edge
|
||||
key={edge.id}
|
||||
edge={edge}
|
||||
props={props}
|
||||
nodes={nodes}
|
||||
selectedElements={selectedElements}
|
||||
elementsSelectable={elementsSelectable}
|
||||
transform={transform}
|
||||
width={width}
|
||||
height={height}
|
||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||
/>
|
||||
))}
|
||||
{renderConnectionLine && (
|
||||
<ConnectionLine
|
||||
nodes={nodes}
|
||||
|
||||
@@ -79,6 +79,7 @@ const GraphView = ({
|
||||
onPaneClick,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
onEdgeUpdate,
|
||||
}: GraphViewProps) => {
|
||||
const isInitialised = useRef<boolean>(false);
|
||||
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
||||
@@ -236,6 +237,7 @@ const GraphView = ({
|
||||
arrowHeadColor={arrowHeadColor}
|
||||
markerEndId={markerEndId}
|
||||
connectionLineComponent={connectionLineComponent}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||
/>
|
||||
</FlowRenderer>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
TranslateExtent,
|
||||
KeyCode,
|
||||
PanOnScrollMode,
|
||||
OnEdgeUpdateFunc,
|
||||
} from '../../types';
|
||||
|
||||
import '../../style.css';
|
||||
@@ -99,6 +100,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
||||
panOnScrollSpeed?: number;
|
||||
panOnScrollMode?: PanOnScrollMode;
|
||||
zoomOnDoubleClick?: boolean;
|
||||
onEdgeUpdate?: OnEdgeUpdateFunc;
|
||||
}
|
||||
|
||||
const ReactFlow = ({
|
||||
@@ -157,6 +159,7 @@ const ReactFlow = ({
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
children,
|
||||
onEdgeUpdate,
|
||||
...rest
|
||||
}: ReactFlowProps) => {
|
||||
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
||||
@@ -218,6 +221,7 @@ const ReactFlow = ({
|
||||
onSelectionDrag={onSelectionDrag}
|
||||
onSelectionDragStop={onSelectionDragStop}
|
||||
onSelectionContextMenu={onSelectionContextMenu}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
/>
|
||||
<ElementUpdater elements={elements} />
|
||||
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
||||
|
||||
@@ -189,7 +189,10 @@ const ZoomPane = ({
|
||||
}
|
||||
|
||||
// when the target element is a node, we still allow zooming
|
||||
if (event.target.closest('.react-flow__node') && event.type !== 'wheel') {
|
||||
if (
|
||||
(event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edgeupdater')) &&
|
||||
event.type !== 'wheel'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -8,7 +8,16 @@ 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 { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
|
||||
|
||||
export * from './additional-components';
|
||||
|
||||
+1
-1
@@ -144,10 +144,10 @@ export const storeModel: StoreModel = {
|
||||
transform: [0, 0, 1],
|
||||
elements: [],
|
||||
nodes: computed((state) => state.elements.filter(isNode)),
|
||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
||||
edges: computed((state) => state.elements.filter(isEdge)),
|
||||
selectedElements: null,
|
||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
||||
|
||||
d3Zoom: null,
|
||||
d3Selection: null,
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
|
||||
&.updating {
|
||||
.react-flow__edge-path {
|
||||
stroke: #777;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dashdraw {
|
||||
@@ -236,3 +242,7 @@
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.react-flow__edgeupdater {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
+6
-1
@@ -111,7 +111,8 @@ export interface WrapEdgeProps {
|
||||
arrowHeadType?: ArrowHeadType;
|
||||
source: ElementId;
|
||||
target: ElementId;
|
||||
sourceHandleId?: string;
|
||||
sourceHandleId: ElementId | null;
|
||||
targetHandleId: ElementId | null;
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
targetX: number;
|
||||
@@ -121,6 +122,8 @@ export interface WrapEdgeProps {
|
||||
elementsSelectable?: boolean;
|
||||
markerEndId?: string;
|
||||
isHidden?: boolean;
|
||||
handleEdgeUpdate: boolean;
|
||||
onConnectEdge: OnConnectFunc;
|
||||
}
|
||||
|
||||
export interface EdgeProps {
|
||||
@@ -348,3 +351,5 @@ export interface ZoomPanHelperFunctions {
|
||||
fitView: (params?: FitViewParams) => void;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export type OnEdgeUpdateFunc = (oldEdge: Edge, newConnection: Connection) => void;
|
||||
|
||||
@@ -87,6 +87,32 @@ 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) {
|
||||
console.warn("Can't create new edge. An edge needs a source and a target.");
|
||||
return elements;
|
||||
}
|
||||
|
||||
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id) as Edge;
|
||||
|
||||
if (!foundEdge) {
|
||||
console.warn(`The old edge with id=${oldEdge.id} does not exist.`);
|
||||
return elements;
|
||||
}
|
||||
|
||||
// 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,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle,
|
||||
} 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