Merge pull request #726 from wbkd/feat/dragedge

Feat/dragedge
This commit is contained in:
Moritz Klack
2020-11-30 11:13:07 +01:00
committed by GitHub
14 changed files with 8725 additions and 46 deletions
+89
View File
@@ -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;
+6
View File
@@ -15,6 +15,7 @@ import Hidden from './Hidden';
import EdgeTypes from './EdgeTypes'; import EdgeTypes from './EdgeTypes';
import CustomConnectionLine from './CustomConnectionLine'; import CustomConnectionLine from './CustomConnectionLine';
import NodeTypeChange from './NodeTypeChange'; import NodeTypeChange from './NodeTypeChange';
import DraggableEdge from './DraggableEdge';
import './index.css'; import './index.css';
@@ -78,6 +79,11 @@ const routes = [
path: '/nodetype-change', path: '/nodetype-change',
component: NodeTypeChange, component: NodeTypeChange,
}, },
{
path: '/draggable-edge',
component: DraggableEdge,
label: 'Draggable Edge',
},
]; ];
const navLinks = routes.filter((route) => route.label); const navLinks = routes.filter((route) => route.label);
+8424 -11
View File
File diff suppressed because it is too large Load Diff
+80 -2
View File
@@ -1,8 +1,9 @@
import React, { memo, ComponentType, useCallback } from 'react'; import React, { memo, ComponentType, useCallback, useState } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { useStoreActions } from '../../store/hooks'; import { useStoreActions } from '../../store/hooks';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
import { onMouseDown } from '../../components/Handle/BaseHandle';
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
@@ -32,15 +33,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
elementsSelectable, elementsSelectable,
markerEndId, markerEndId,
isHidden, isHidden,
sourceHandleId,
targetHandleId,
handleEdgeUpdate,
onConnectEdge,
}: WrapEdgeProps) => { }: WrapEdgeProps) => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); 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 inactive = !elementsSelectable && !onClick;
const edgeClasses = cc([ const edgeClasses = cc([
'react-flow__edge', 'react-flow__edge',
`react-flow__edge-${type}`, `react-flow__edge-${type}`,
className, className,
{ selected, animated, inactive }, { selected, animated, inactive, updating },
]); ]);
const onEdgeClick = useCallback( const onEdgeClick = useCallback(
@@ -62,12 +70,66 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
[elementsSelectable, id, source, target, type, data, onClick] [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) { if (isHidden) {
return null; return null;
} }
return ( return (
<g className={edgeClasses} onClick={onEdgeClick}> <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 <EdgeComponent
id={id} id={id}
source={source} source={source}
@@ -91,6 +153,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
targetPosition={targetPosition} targetPosition={targetPosition}
markerEndId={markerEndId} 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> </g>
); );
}; };
+3 -2
View File
@@ -15,7 +15,7 @@ import {
} from '../../types'; } from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean; type ValidConnectionFunc = (connection: Connection) => boolean;
type SetSourceIdFunc = (params: SetConnectionId) => void; export type SetSourceIdFunc = (params: SetConnectionId) => void;
interface BaseHandleProps { interface BaseHandleProps {
type: HandleType; type: HandleType;
@@ -40,7 +40,7 @@ type Result = {
isHoveringHandle: boolean; isHoveringHandle: boolean;
}; };
function onMouseDown( export function onMouseDown(
event: ReactMouseEvent, event: ReactMouseEvent,
handleId: ElementId | null, handleId: ElementId | null,
nodeId: ElementId, nodeId: ElementId,
@@ -67,6 +67,7 @@ function onMouseDown(
x: event.clientX - containerBounds.left, x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top, y: event.clientY - containerBounds.top,
}); });
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType }); setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
if (onConnectStart) { if (onConnectStart) {
+60 -27
View File
@@ -1,11 +1,21 @@
import React, { memo, CSSProperties } from 'react'; import React, { memo, CSSProperties, useCallback } from 'react';
import { useStoreState } from '../../store/hooks'; import { useStoreState } from '../../store/hooks';
import ConnectionLine from '../../components/ConnectionLine/index'; import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph'; import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils'; 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 { interface EdgeRendererProps {
edgeTypes: any; edgeTypes: any;
@@ -16,23 +26,43 @@ interface EdgeRendererProps {
markerEndId?: string; markerEndId?: string;
connectionLineComponent?: ConnectionLineComponent; connectionLineComponent?: ConnectionLineComponent;
onlyRenderVisibleElements: boolean; onlyRenderVisibleElements: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
} }
function renderEdge( interface EdgeWrapperProps {
edge: Edge, edge: Edge;
props: EdgeRendererProps, props: EdgeRendererProps;
nodes: Node[], nodes: Node[];
selectedElements: Elements | null, selectedElements: Elements | null;
elementsSelectable: boolean, elementsSelectable: boolean;
transform: Transform, transform: Transform;
width: number, width: number;
height: number, height: number;
onlyRenderVisibleElements: boolean onlyRenderVisibleElements: boolean;
) { }
const Edge = ({
edge,
props,
nodes,
selectedElements,
elementsSelectable,
transform,
width,
height,
onlyRenderVisibleElements,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null; const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null; const targetHandleId = edge.targetHandle || null;
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
const onConnectEdge = useCallback(
(connection: Connection) => {
props.onEdgeUpdate?.(edge, connection);
},
[edge]
);
if (!sourceNode) { if (!sourceNode) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null; return null;
@@ -120,9 +150,11 @@ function renderEdge(
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
markerEndId={props.markerEndId} markerEndId={props.markerEndId}
isHidden={edge.isHidden} isHidden={edge.isHidden}
onConnectEdge={onConnectEdge}
handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'}
/> />
); );
} };
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = (props: EdgeRendererProps) => {
const transform = useStoreState((state) => state.transform); const transform = useStoreState((state) => state.transform);
@@ -156,19 +188,20 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<svg width={width} height={height} className="react-flow__edges"> <svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} /> <MarkerDefinitions color={arrowHeadColor} />
<g transform={transformStyle}> <g transform={transformStyle}>
{edges.map((edge: Edge) => {edges.map((edge: Edge) => (
renderEdge( <Edge
edge, key={edge.id}
props, edge={edge}
nodes, props={props}
selectedElements, nodes={nodes}
elementsSelectable, selectedElements={selectedElements}
transform, elementsSelectable={elementsSelectable}
width, transform={transform}
height, width={width}
onlyRenderVisibleElements height={height}
) onlyRenderVisibleElements={onlyRenderVisibleElements}
)} />
))}
{renderConnectionLine && ( {renderConnectionLine && (
<ConnectionLine <ConnectionLine
nodes={nodes} nodes={nodes}
+2
View File
@@ -79,6 +79,7 @@ const GraphView = ({
onPaneClick, onPaneClick,
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
onEdgeUpdate,
}: GraphViewProps) => { }: GraphViewProps) => {
const isInitialised = useRef<boolean>(false); const isInitialised = useRef<boolean>(false);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
@@ -236,6 +237,7 @@ const GraphView = ({
arrowHeadColor={arrowHeadColor} arrowHeadColor={arrowHeadColor}
markerEndId={markerEndId} markerEndId={markerEndId}
connectionLineComponent={connectionLineComponent} connectionLineComponent={connectionLineComponent}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
/> />
</FlowRenderer> </FlowRenderer>
+4
View File
@@ -28,6 +28,7 @@ import {
TranslateExtent, TranslateExtent,
KeyCode, KeyCode,
PanOnScrollMode, PanOnScrollMode,
OnEdgeUpdateFunc,
} from '../../types'; } from '../../types';
import '../../style.css'; import '../../style.css';
@@ -99,6 +100,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
panOnScrollSpeed?: number; panOnScrollSpeed?: number;
panOnScrollMode?: PanOnScrollMode; panOnScrollMode?: PanOnScrollMode;
zoomOnDoubleClick?: boolean; zoomOnDoubleClick?: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
} }
const ReactFlow = ({ const ReactFlow = ({
@@ -157,6 +159,7 @@ const ReactFlow = ({
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
children, children,
onEdgeUpdate,
...rest ...rest
}: ReactFlowProps) => { }: ReactFlowProps) => {
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []); const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
@@ -218,6 +221,7 @@ const ReactFlow = ({
onSelectionDrag={onSelectionDrag} onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop} onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu} onSelectionContextMenu={onSelectionContextMenu}
onEdgeUpdate={onEdgeUpdate}
/> />
<ElementUpdater elements={elements} /> <ElementUpdater elements={elements} />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />} {onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
+4 -1
View File
@@ -189,7 +189,10 @@ const ZoomPane = ({
} }
// when the target element is a node, we still allow zooming // 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; return false;
} }
+10 -1
View File
@@ -8,7 +8,16 @@ export { getBezierPath } from './components/Edges/BezierEdge';
export { getSmoothStepPath } from './components/Edges/SmoothStepEdge'; export { getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'; 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 { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export * from './additional-components'; export * from './additional-components';
+1 -1
View File
@@ -144,10 +144,10 @@ export const storeModel: StoreModel = {
transform: [0, 0, 1], transform: [0, 0, 1],
elements: [], elements: [],
nodes: computed((state) => state.elements.filter(isNode)), 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)), edges: computed((state) => state.elements.filter(isEdge)),
selectedElements: null, selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
d3Zoom: null, d3Zoom: null,
d3Selection: null, d3Selection: null,
+10
View File
@@ -60,6 +60,12 @@
stroke-dasharray: 5; stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite; animation: dashdraw 0.5s linear infinite;
} }
&.updating {
.react-flow__edge-path {
stroke: #777;
}
}
} }
@keyframes dashdraw { @keyframes dashdraw {
@@ -236,3 +242,7 @@
top: 50%; top: 50%;
transform: translate(0, -50%); transform: translate(0, -50%);
} }
.react-flow__edgeupdater {
cursor: move;
}
+6 -1
View File
@@ -111,7 +111,8 @@ export interface WrapEdgeProps {
arrowHeadType?: ArrowHeadType; arrowHeadType?: ArrowHeadType;
source: ElementId; source: ElementId;
target: ElementId; target: ElementId;
sourceHandleId?: string; sourceHandleId: ElementId | null;
targetHandleId: ElementId | null;
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
targetX: number; targetX: number;
@@ -121,6 +122,8 @@ export interface WrapEdgeProps {
elementsSelectable?: boolean; elementsSelectable?: boolean;
markerEndId?: string; markerEndId?: string;
isHidden?: boolean; isHidden?: boolean;
handleEdgeUpdate: boolean;
onConnectEdge: OnConnectFunc;
} }
export interface EdgeProps { export interface EdgeProps {
@@ -348,3 +351,5 @@ export interface ZoomPanHelperFunctions {
fitView: (params?: FitViewParams) => void; fitView: (params?: FitViewParams) => void;
initialized: boolean; initialized: boolean;
} }
export type OnEdgeUpdateFunc = (oldEdge: Edge, newConnection: Connection) => void;
+26
View File
@@ -87,6 +87,32 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
return elements.concat(edge); 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 = ( export const pointToRendererPoint = (
{ x, y }: XYPosition, { x, y }: XYPosition,
[tx, ty, tScale]: Transform, [tx, ty, tScale]: Transform,