Skip to content

Commit 661caa3

Browse files
authored
feat: freeze edges while dragging associated nodes (#310)
1 parent 513de9e commit 661caa3

File tree

8 files changed

+137
-12
lines changed

8 files changed

+137
-12
lines changed

packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,47 @@ export const EdgeStyles = withTopologySetup(() => {
666666
return null;
667667
});
668668

669+
export const FreezedEdge = withTopologySetup(() => {
670+
useComponentFactory(defaultComponentFactory);
671+
useComponentFactory(stylesComponentFactory);
672+
const nodes: NodeModel[] = createGroupNodes('edges-group');
673+
const edges: EdgeModel[] = [];
674+
675+
const middleNodeIndex = nodes.length - 1;
676+
nodes.forEach((item, index) => {
677+
if (index === middleNodeIndex) {
678+
return;
679+
}
680+
const endIndex = index < nodes.length - 2 ? index + 1 : 0;
681+
edges.push({
682+
id: `edge-${index}-${endIndex}`,
683+
type: 'edge',
684+
source: nodes[index].id,
685+
target: nodes[endIndex].id,
686+
edgeStyle: EDGE_STYLES[index % EDGE_STYLE_COUNT],
687+
data: { freezeEdgeDuringNodeDrag: true }
688+
});
689+
edges.push({
690+
id: `edge-${middleNodeIndex}-${index}`,
691+
type: 'edge',
692+
source: nodes[middleNodeIndex].id,
693+
target: nodes[index].id,
694+
edgeStyle: EdgeStyle.default,
695+
data: { freezeEdgeDuringNodeDrag: true }
696+
});
697+
});
698+
699+
useModel({
700+
graph: {
701+
id: 'g1',
702+
type: 'graph'
703+
},
704+
nodes,
705+
edges
706+
});
707+
return null;
708+
});
709+
669710
export const EdgeAnimationStyles = withTopologySetup(() => {
670711
useComponentFactory(defaultComponentFactory);
671712
useComponentFactory(stylesComponentFactory);
@@ -977,16 +1018,19 @@ export const StyleEdges: React.FunctionComponent = () => {
9771018
<Tab eventKey={0} title={<TabTitleText>Edge Styles</TabTitleText>}>
9781019
<EdgeStyles />
9791020
</Tab>
980-
<Tab eventKey={1} title={<TabTitleText>Animated Edges</TabTitleText>}>
1021+
<Tab eventKey={1} title={<TabTitleText>Freeze Edge When Dragging Node</TabTitleText>}>
1022+
<FreezedEdge />
1023+
</Tab>
1024+
<Tab eventKey={2} title={<TabTitleText>Animated Edges</TabTitleText>}>
9811025
<EdgeAnimationStyles />
9821026
</Tab>
983-
<Tab eventKey={2} title={<TabTitleText>Edge Terminal Types</TabTitleText>}>
1027+
<Tab eventKey={3} title={<TabTitleText>Edge Terminal Types</TabTitleText>}>
9841028
<EdgeTerminalStyles />
9851029
</Tab>
986-
<Tab eventKey={3} title={<TabTitleText>Edge Terminal Status</TabTitleText>}>
1030+
<Tab eventKey={4} title={<TabTitleText>Edge Terminal Status</TabTitleText>}>
9871031
<EdgeTerminalStatusStyles />
9881032
</Tab>
989-
<Tab eventKey={4} title={<TabTitleText>Edge Terminal Tags</TabTitleText>}>
1033+
<Tab eventKey={5} title={<TabTitleText>Edge Terminal Tags</TabTitleText>}>
9901034
<EdgeTerminalTagStyles />
9911035
</Tab>
9921036
</Tabs>

packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
groupDropTargetSpec,
2424
graphDropTargetSpec,
2525
NODE_DRAG_TYPE,
26-
CREATE_CONNECTOR_DROP_TYPE
26+
CREATE_CONNECTOR_DROP_TYPE,
27+
edgeDropTargetSpec
2728
} from '@patternfly/react-topology';
2829
import StyleNode from './StyleNode';
2930
import StyleGroup from './StyleGroup';
@@ -132,7 +133,11 @@ const stylesComponentFactory: ComponentFactory = (
132133
collect: (monitor) => ({
133134
dragging: monitor.isDragging()
134135
})
135-
})(withContextMenu(() => defaultMenu)(withSelection()(StyleEdge)))
136+
})(
137+
withDndDrop<Node, any, { droppable?: boolean; hover?: boolean; canDrop?: boolean }, EdgeProps>(
138+
edgeDropTargetSpec
139+
)(withContextMenu(() => defaultMenu)(withSelection()(StyleEdge)))
140+
)
136141
);
137142
default:
138143
return undefined;

packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export class DemoModel {
2222
showStatus: false,
2323
showAnimations: false,
2424
showTags: false,
25-
terminalTypes: false
25+
terminalTypes: false,
26+
freezeEdgeDuringNodeDrag: false
2627
};
2728
protected creationCountsP: { numNodes: number; numEdges: number; numGroups: number; nestedLevel: number } = {
2829
numNodes: 6,

packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const DemoEdge: React.FunctionComponent<DemoEdgeProps> = ({ element, ...rest })
3737
endTerminalStatus={options.showStatus && NODE_STATUSES[data.index % NODE_STATUSES.length]}
3838
tag={options.showTags ? data.tag : undefined}
3939
tagStatus={options.showStatus && NODE_STATUSES[data.index % NODE_STATUSES.length]}
40+
freezeEdgeDuringNodeDrag={options.freezeEdgeDuringNodeDrag}
4041
/>
4142
);
4243
};

packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,19 @@ const OptionsContextBar: React.FC = observer(() => {
202202
>
203203
Tags
204204
</SelectOption>
205+
<SelectOption
206+
value="Freeze Edge During Node Drag"
207+
hasCheckbox
208+
isSelected={options.edgeOptions.freezeEdgeDuringNodeDrag}
209+
onClick={() =>
210+
options.setEdgeOptions({
211+
...options.edgeOptions,
212+
freezeEdgeDuringNodeDrag: !options.edgeOptions.freezeEdgeDuringNodeDrag
213+
})
214+
}
215+
>
216+
Freeze Edge During Node Drag
217+
</SelectOption>
205218
</SelectList>
206219
);
207220
const edgeOptionsToggle = (toggleRef: React.Ref<MenuToggleElement>) => (

packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export interface GeneratorEdgeOptions {
9999
showAnimations?: boolean;
100100
showTags?: boolean;
101101
terminalTypes?: boolean;
102+
freezeEdgeDuringNodeDrag?: boolean;
102103
}
103104

104105
const createNode = (index: number): NodeModel => ({

packages/module/src/components/edges/DefaultEdge.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLayoutEffect } from 'react';
1+
import { useLayoutEffect, useRef } from 'react';
22
import { observer } from 'mobx-react';
33
import {
44
Edge,
@@ -71,6 +71,8 @@ interface DefaultEdgeProps {
7171
canDrop?: boolean;
7272
/** Flag if the node is the current drop target */
7373
dropTarget?: boolean;
74+
/** Flag indicating if an edge endpoint is currently being dragged */
75+
endpointDragging?: boolean;
7476
/** Flag indicating if the element is selected. Part of WithSelectionProps */
7577
selected?: boolean;
7678
/** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */
@@ -79,6 +81,13 @@ interface DefaultEdgeProps {
7981
onContextMenu?: (e: React.MouseEvent) => void;
8082
/** Flag indicating that the context menu for the edge is currently open */
8183
contextMenuOpen?: boolean;
84+
/**
85+
* When true, the edge path and terminals stay visually fixed (last position) while
86+
* an associated node is being dragged. When false or omitted, the edge updates
87+
* during drag as before. Can be set per edge via this prop, element state, or
88+
* in the model as edge data: `data: { freezeEdgeDuringNodeDrag: true }`.
89+
*/
90+
freezeEdgeDuringNodeDrag?: boolean;
8291
}
8392

8493
type DefaultEdgeInnerProps = Omit<DefaultEdgeProps, 'element'> & { element: Edge };
@@ -92,6 +101,7 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
92101
dndDropRef,
93102
canDrop,
94103
dropTarget,
104+
endpointDragging,
95105
edgeStyle,
96106
animationDuration,
97107
onShowRemoveConnector,
@@ -111,12 +121,23 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
111121
className,
112122
selected,
113123
onSelect,
114-
onContextMenu
124+
onContextMenu,
125+
freezeEdgeDuringNodeDrag
115126
}) => {
127+
const freezeDuringDrag =
128+
freezeEdgeDuringNodeDrag ??
129+
(element.getData() as { freezeEdgeDuringNodeDrag?: boolean } | undefined)?.freezeEdgeDuringNodeDrag ??
130+
false;
131+
116132
const [hover, hoverRef] = useHover();
117133
const edgeRef = useCombineRefs(hoverRef, dndDropRef);
118134
const startPoint = element.getStartPoint();
119135
const endPoint = element.getEndPoint();
136+
const edgeDRef = useRef<string | null>(null);
137+
const startPointRef = useRef<Point | null>(null);
138+
const endPointRef = useRef<Point | null>(null);
139+
const targetStartPointRef = useRef<Point | null>(null);
140+
const targetEndPointRef = useRef<Point | null>(null);
120141

121142
useLayoutEffect(() => {
122143
if (hover && !dragging) {
@@ -175,6 +196,23 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
175196
const tagScale = hover && !(detailsLevel === ScaleDetailsLevel.high) ? Math.max(1, 1 / scale) : 1;
176197
const tagPositionScale = hover && !(detailsLevel === ScaleDetailsLevel.high) ? Math.min(1, scale) : 1;
177198

199+
const shouldFreeze = freezeDuringDrag && endpointDragging;
200+
201+
if (
202+
!shouldFreeze ||
203+
!edgeDRef.current ||
204+
!startPointRef.current ||
205+
!endPointRef.current ||
206+
!targetStartPointRef.current ||
207+
!targetEndPointRef.current
208+
) {
209+
edgeDRef.current = d;
210+
startPointRef.current = bendpoints[0] || endPoint;
211+
endPointRef.current = startPoint;
212+
targetStartPointRef.current = bendpoints[bendpoints.length - 1] || startPoint;
213+
targetEndPointRef.current = endPoint;
214+
}
215+
178216
return (
179217
<Layer id={dragging || hover ? TOP_LAYER : undefined}>
180218
<g
@@ -190,7 +228,11 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
190228
onMouseEnter={onShowRemoveConnector}
191229
onMouseLeave={onHideRemoveConnector}
192230
/>
193-
<path className={linkClassName} d={d} style={{ animationDuration: `${edgeAnimationDuration}s` }} />
231+
<path
232+
className={linkClassName}
233+
d={edgeDRef.current}
234+
style={{ animationDuration: `${edgeAnimationDuration}s` }}
235+
/>
194236
{showTag && (
195237
<g transform={`scale(${hover ? tagScale : 1})`}>
196238
<DefaultConnectorTag
@@ -211,6 +253,8 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
211253
terminalType={startTerminalType}
212254
status={startTerminalStatus}
213255
highlight={dragging || hover}
256+
startPoint={shouldFreeze ? startPointRef.current : undefined}
257+
endPoint={shouldFreeze ? endPointRef.current : undefined}
214258
/>
215259
<DefaultConnectorTerminal
216260
className={endTerminalClass}
@@ -221,6 +265,8 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
221265
terminalType={endTerminalType}
222266
status={endTerminalStatus}
223267
highlight={dragging || hover}
268+
startPoint={shouldFreeze ? targetStartPointRef.current : undefined}
269+
endPoint={shouldFreeze ? targetEndPointRef.current : undefined}
224270
/>
225271
{children}
226272
</g>

packages/module/src/components/factories/components/componentUtils.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,31 @@ const edgeDragSourceSpec = (
278278
})
279279
});
280280

281+
const edgeEndpointIsDragging = (monitor: any, props: EdgeComponentProps) => {
282+
if (!monitor.isDragging()) {
283+
return false;
284+
}
285+
if (monitor.getItemType() === NODE_DRAG_TYPE) {
286+
return (
287+
monitor.getItem().element.id === props.element.getSource().getId() ||
288+
monitor.getItem().element.id === props.element.getTarget().getId()
289+
);
290+
}
291+
return false;
292+
};
293+
281294
const edgeDropTargetSpec: DropTargetSpec<any, any, { droppable: boolean; dropTarget: boolean; canDrop: boolean }, any> =
282295
{
283296
accept: [NODE_DRAG_TYPE],
284297
canDrop: (item, monitor, props) =>
285298
!!props &&
286299
(props.element as Edge).getSource().getId() !== item.id &&
287300
(props.element as Edge).getTarget().getId() !== item.id,
288-
collect: (monitor) => ({
301+
collect: (monitor, props) => ({
289302
droppable: monitor.isDragging(),
290303
dropTarget: monitor.isOver(),
291-
canDrop: monitor.canDrop()
304+
canDrop: monitor.canDrop(),
305+
endpointDragging: edgeEndpointIsDragging(monitor, { element: props.element as Edge })
292306
})
293307
};
294308

0 commit comments

Comments
 (0)