diff --git a/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx index e5b7f9d1..3c3f70e7 100644 --- a/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx +++ b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx @@ -666,6 +666,47 @@ export const EdgeStyles = withTopologySetup(() => { return null; }); +export const FreezedEdge = withTopologySetup(() => { + useComponentFactory(defaultComponentFactory); + useComponentFactory(stylesComponentFactory); + const nodes: NodeModel[] = createGroupNodes('edges-group'); + const edges: EdgeModel[] = []; + + const middleNodeIndex = nodes.length - 1; + nodes.forEach((item, index) => { + if (index === middleNodeIndex) { + return; + } + const endIndex = index < nodes.length - 2 ? index + 1 : 0; + edges.push({ + id: `edge-${index}-${endIndex}`, + type: 'edge', + source: nodes[index].id, + target: nodes[endIndex].id, + edgeStyle: EDGE_STYLES[index % EDGE_STYLE_COUNT], + data: { freezeEdgeDuringNodeDrag: true } + }); + edges.push({ + id: `edge-${middleNodeIndex}-${index}`, + type: 'edge', + source: nodes[middleNodeIndex].id, + target: nodes[index].id, + edgeStyle: EdgeStyle.default, + data: { freezeEdgeDuringNodeDrag: true } + }); + }); + + useModel({ + graph: { + id: 'g1', + type: 'graph' + }, + nodes, + edges + }); + return null; +}); + export const EdgeAnimationStyles = withTopologySetup(() => { useComponentFactory(defaultComponentFactory); useComponentFactory(stylesComponentFactory); @@ -977,16 +1018,19 @@ export const StyleEdges: React.FunctionComponent = () => { Edge Styles}> - Animated Edges}> + Freeze Edge When Dragging Node}> + + + Animated Edges}> - Edge Terminal Types}> + Edge Terminal Types}> - Edge Terminal Status}> + Edge Terminal Status}> - Edge Terminal Tags}> + Edge Terminal Tags}> diff --git a/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx index 7cce6ce5..5cc5ef8e 100644 --- a/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx @@ -23,7 +23,8 @@ import { groupDropTargetSpec, graphDropTargetSpec, NODE_DRAG_TYPE, - CREATE_CONNECTOR_DROP_TYPE + CREATE_CONNECTOR_DROP_TYPE, + edgeDropTargetSpec } from '@patternfly/react-topology'; import StyleNode from './StyleNode'; import StyleGroup from './StyleGroup'; @@ -132,7 +133,11 @@ const stylesComponentFactory: ComponentFactory = ( collect: (monitor) => ({ dragging: monitor.isDragging() }) - })(withContextMenu(() => defaultMenu)(withSelection()(StyleEdge))) + })( + withDndDrop( + edgeDropTargetSpec + )(withContextMenu(() => defaultMenu)(withSelection()(StyleEdge))) + ) ); default: return undefined; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx index f92fe388..7badd985 100644 --- a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx @@ -22,7 +22,8 @@ export class DemoModel { showStatus: false, showAnimations: false, showTags: false, - terminalTypes: false + terminalTypes: false, + freezeEdgeDuringNodeDrag: false }; protected creationCountsP: { numNodes: number; numEdges: number; numGroups: number; nestedLevel: number } = { numNodes: 6, diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx index 5ca3a46d..eeda41a9 100644 --- a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx @@ -37,6 +37,7 @@ const DemoEdge: React.FunctionComponent = ({ element, ...rest }) endTerminalStatus={options.showStatus && NODE_STATUSES[data.index % NODE_STATUSES.length]} tag={options.showTags ? data.tag : undefined} tagStatus={options.showStatus && NODE_STATUSES[data.index % NODE_STATUSES.length]} + freezeEdgeDuringNodeDrag={options.freezeEdgeDuringNodeDrag} /> ); }; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx index 7081bb91..57d436ce 100644 --- a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx @@ -202,6 +202,19 @@ const OptionsContextBar: React.FC = observer(() => { > Tags + + options.setEdgeOptions({ + ...options.edgeOptions, + freezeEdgeDuringNodeDrag: !options.edgeOptions.freezeEdgeDuringNodeDrag + }) + } + > + Freeze Edge During Node Drag + ); const edgeOptionsToggle = (toggleRef: React.Ref) => ( diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts index 2eed6fb5..880462d4 100644 --- a/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts @@ -99,6 +99,7 @@ export interface GeneratorEdgeOptions { showAnimations?: boolean; showTags?: boolean; terminalTypes?: boolean; + freezeEdgeDuringNodeDrag?: boolean; } const createNode = (index: number): NodeModel => ({ diff --git a/packages/module/src/components/edges/DefaultEdge.tsx b/packages/module/src/components/edges/DefaultEdge.tsx index aa38bba9..8dd87482 100644 --- a/packages/module/src/components/edges/DefaultEdge.tsx +++ b/packages/module/src/components/edges/DefaultEdge.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect } from 'react'; +import { useLayoutEffect, useRef } from 'react'; import { observer } from 'mobx-react'; import { Edge, @@ -71,6 +71,8 @@ interface DefaultEdgeProps { canDrop?: boolean; /** Flag if the node is the current drop target */ dropTarget?: boolean; + /** Flag indicating if an edge endpoint is currently being dragged */ + endpointDragging?: boolean; /** Flag indicating if the element is selected. Part of WithSelectionProps */ selected?: boolean; /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ @@ -79,6 +81,13 @@ interface DefaultEdgeProps { onContextMenu?: (e: React.MouseEvent) => void; /** Flag indicating that the context menu for the edge is currently open */ contextMenuOpen?: boolean; + /** + * When true, the edge path and terminals stay visually fixed (last position) while + * an associated node is being dragged. When false or omitted, the edge updates + * during drag as before. Can be set per edge via this prop, element state, or + * in the model as edge data: `data: { freezeEdgeDuringNodeDrag: true }`. + */ + freezeEdgeDuringNodeDrag?: boolean; } type DefaultEdgeInnerProps = Omit & { element: Edge }; @@ -92,6 +101,7 @@ const DefaultEdgeInner: React.FunctionComponent = observe dndDropRef, canDrop, dropTarget, + endpointDragging, edgeStyle, animationDuration, onShowRemoveConnector, @@ -111,12 +121,23 @@ const DefaultEdgeInner: React.FunctionComponent = observe className, selected, onSelect, - onContextMenu + onContextMenu, + freezeEdgeDuringNodeDrag }) => { + const freezeDuringDrag = + freezeEdgeDuringNodeDrag ?? + (element.getData() as { freezeEdgeDuringNodeDrag?: boolean } | undefined)?.freezeEdgeDuringNodeDrag ?? + false; + const [hover, hoverRef] = useHover(); const edgeRef = useCombineRefs(hoverRef, dndDropRef); const startPoint = element.getStartPoint(); const endPoint = element.getEndPoint(); + const edgeDRef = useRef(null); + const startPointRef = useRef(null); + const endPointRef = useRef(null); + const targetStartPointRef = useRef(null); + const targetEndPointRef = useRef(null); useLayoutEffect(() => { if (hover && !dragging) { @@ -175,6 +196,23 @@ const DefaultEdgeInner: React.FunctionComponent = observe const tagScale = hover && !(detailsLevel === ScaleDetailsLevel.high) ? Math.max(1, 1 / scale) : 1; const tagPositionScale = hover && !(detailsLevel === ScaleDetailsLevel.high) ? Math.min(1, scale) : 1; + const shouldFreeze = freezeDuringDrag && endpointDragging; + + if ( + !shouldFreeze || + !edgeDRef.current || + !startPointRef.current || + !endPointRef.current || + !targetStartPointRef.current || + !targetEndPointRef.current + ) { + edgeDRef.current = d; + startPointRef.current = bendpoints[0] || endPoint; + endPointRef.current = startPoint; + targetStartPointRef.current = bendpoints[bendpoints.length - 1] || startPoint; + targetEndPointRef.current = endPoint; + } + return ( = observe onMouseEnter={onShowRemoveConnector} onMouseLeave={onHideRemoveConnector} /> - + {showTag && ( = observe terminalType={startTerminalType} status={startTerminalStatus} highlight={dragging || hover} + startPoint={shouldFreeze ? startPointRef.current : undefined} + endPoint={shouldFreeze ? endPointRef.current : undefined} /> = observe terminalType={endTerminalType} status={endTerminalStatus} highlight={dragging || hover} + startPoint={shouldFreeze ? targetStartPointRef.current : undefined} + endPoint={shouldFreeze ? targetEndPointRef.current : undefined} /> {children} diff --git a/packages/module/src/components/factories/components/componentUtils.ts b/packages/module/src/components/factories/components/componentUtils.ts index ee6e3c46..11bb5be4 100644 --- a/packages/module/src/components/factories/components/componentUtils.ts +++ b/packages/module/src/components/factories/components/componentUtils.ts @@ -278,6 +278,19 @@ const edgeDragSourceSpec = ( }) }); +const edgeEndpointIsDragging = (monitor: any, props: EdgeComponentProps) => { + if (!monitor.isDragging()) { + return false; + } + if (monitor.getItemType() === NODE_DRAG_TYPE) { + return ( + monitor.getItem().element.id === props.element.getSource().getId() || + monitor.getItem().element.id === props.element.getTarget().getId() + ); + } + return false; +}; + const edgeDropTargetSpec: DropTargetSpec = { accept: [NODE_DRAG_TYPE], @@ -285,10 +298,11 @@ const edgeDropTargetSpec: DropTargetSpec ({ + collect: (monitor, props) => ({ droppable: monitor.isDragging(), dropTarget: monitor.isOver(), - canDrop: monitor.canDrop() + canDrop: monitor.canDrop(), + endpointDragging: edgeEndpointIsDragging(monitor, { element: props.element as Edge }) }) };