Skip to content

Commit 540b8b0

Browse files
committed
feat: show ghost node during node drag
1 parent 661caa3 commit 540b8b0

File tree

8 files changed

+129
-30
lines changed

8 files changed

+129
-30
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,32 @@ export const NodeDecoratorStyles = withTopologySetup(() => {
132132
return null;
133133
});
134134

135+
export const NodeShowDragGhostStyles = withTopologySetup(() => {
136+
useComponentFactory(defaultComponentFactory);
137+
useComponentFactory(stylesComponentFactory);
138+
const nodes: NodeModel[] = createGroupNodes();
139+
const nodes2: NodeModel[] = createGroupNodes('2', 600);
140+
141+
nodes.forEach((n) => (n.data.showDecorators = true));
142+
nodes.forEach((n) => (n.data.labelPosition = LabelPosition.bottom));
143+
nodes.forEach((n) => (n.data.showDragGhost = true));
144+
nodes2.forEach((n) => (n.data.showDecorators = true));
145+
nodes2.forEach((n) => (n.data.showDragGhost = true));
146+
useModel(
147+
useMemo(
148+
(): Model => ({
149+
graph: {
150+
id: 'g1',
151+
type: 'graph'
152+
},
153+
nodes: [...nodes, ...nodes2]
154+
}),
155+
[nodes, nodes2]
156+
)
157+
);
158+
return null;
159+
});
160+
135161
export const NodeLabelStyles = withTopologySetup(() => {
136162
useComponentFactory(defaultComponentFactory);
137163
useComponentFactory(stylesComponentFactory);
@@ -928,6 +954,9 @@ export const StyleNodes: React.FunctionComponent = () => {
928954
<Tab eventKey={4} title={<TabTitleText>Decorators</TabTitleText>}>
929955
<NodeDecoratorStyles />
930956
</Tab>
957+
<Tab eventKey={5} title={<TabTitleText>Show Drag Ghost While Dragging</TabTitleText>}>
958+
<NodeShowDragGhostStyles />
959+
</Tab>
931960
</Tabs>
932961
</div>
933962
);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export class DemoModel {
1515
badges: false,
1616
icons: false,
1717
contextMenus: false,
18-
hulledOutline: true
18+
hulledOutline: true,
19+
showDragGhost: false
1920
};
2021
protected edgeOptionsP: GeneratorEdgeOptions = {
2122
showStyles: false,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const DemoNode: React.FunctionComponent<DemoNodeProps> = observer(
194194
options.showDecorators &&
195195
renderDecorators(options, nodeElement, rest.getShapeDecoratorCenter)
196196
}
197+
showDragGhost={options.showDragGhost}
197198
>
198199
{(focused || detailsLevel !== ScaleDetailsLevel.low) && renderIcon(data, nodeElement)}
199200
</DefaultNode>

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ const OptionsContextBar: React.FC = observer(() => {
146146
>
147147
Rectangle Groups
148148
</SelectOption>
149+
<SelectOption
150+
hasCheckbox
151+
value="Show Drag Ghost"
152+
isSelected={options.nodeOptions.showDragGhost}
153+
onClick={() =>
154+
options.setNodeOptions({ ...options.nodeOptions, showDragGhost: !options.nodeOptions.showDragGhost })
155+
}
156+
>
157+
Show Drag Ghost
158+
</SelectOption>
149159
</SelectList>
150160
</Select>
151161
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export interface GeneratorNodeOptions {
9191
contextMenus?: boolean;
9292
hideKebabMenu?: boolean;
9393
hulledOutline?: boolean;
94+
showDragGhost?: boolean;
9495
}
9596

9697
export interface GeneratorEdgeOptions {

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ interface DefaultEdgeProps {
8484
/**
8585
* When true, the edge path and terminals stay visually fixed (last position) while
8686
* 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 }`.
87+
* during drag as before.
8988
*/
9089
freezeEdgeDuringNodeDrag?: boolean;
9190
}
@@ -124,10 +123,7 @@ const DefaultEdgeInner: React.FunctionComponent<DefaultEdgeInnerProps> = observe
124123
onContextMenu,
125124
freezeEdgeDuringNodeDrag
126125
}) => {
127-
const freezeDuringDrag =
128-
freezeEdgeDuringNodeDrag ??
129-
(element.getData() as { freezeEdgeDuringNodeDrag?: boolean } | undefined)?.freezeEdgeDuringNodeDrag ??
130-
false;
126+
const freezeDuringDrag = freezeEdgeDuringNodeDrag ?? false;
131127

132128
const [hover, hoverRef] = useHover();
133129
const edgeRef = useCombineRefs(hoverRef, dndDropRef);

packages/module/src/components/nodes/DefaultNode.tsx

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle
66
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
77
import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
88
import styles from '../../css/topology-components';
9-
import { BadgeLocation, GraphElement, isNode, LabelPosition, Node, NodeStatus, TopologyQuadrant } from '../../types';
9+
import {
10+
BadgeLocation,
11+
GraphElement,
12+
isNode,
13+
LabelPosition,
14+
Node,
15+
NodeStatus,
16+
ScaleDetailsLevel,
17+
TopologyQuadrant
18+
} from '../../types';
1019
import { ConnectDragSource, ConnectDropTarget, OnSelect, WithDndDragProps } from '../../behavior';
1120
import Decorator from '../decorators/Decorator';
1221
import { Layer } from '../layers';
@@ -122,10 +131,18 @@ interface DefaultNodeProps {
122131
raiseLabelOnHover?: boolean; // TODO: Update default to be false, assume demo code will be followed
123132
/** Hide context menu kebab for the node */
124133
hideContextMenuKebab?: boolean;
134+
/**
135+
* When true, a non-interactive copy of the node is drawn at the pre-drag position while dragging.
136+
* When false or omitted, only the live node is shown (default behavior).
137+
*/
138+
showDragGhost?: boolean;
125139
}
126140

127141
const SCALE_UP_TIME = 200;
128142

143+
/** Scale factor for the drag ghost when the graph is not at low details level. */
144+
const DRAG_GHOST_SCALE = 0.7;
145+
129146
type DefaultNodeInnerProps = Omit<DefaultNodeProps, 'element'> & { element: Node };
130147

131148
const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observer(
@@ -172,8 +189,10 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
172189
onContextMenu,
173190
contextMenuOpen,
174191
raiseLabelOnHover = true,
175-
hideContextMenuKebab
192+
hideContextMenuKebab,
193+
showDragGhost
176194
}) => {
195+
const showDragGhostResolved = showDragGhost ?? false;
177196
const [nodeHovered, hoverRef] = useHover();
178197
const [labelHovered, labelRef] = useHover();
179198
const hovered = nodeHovered || labelHovered;
@@ -183,6 +202,8 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
183202
const isHover = hover !== undefined ? hover : hovered;
184203
const [nodeScale, setNodeScale] = useState<number>(1);
185204
const decoratorRef = useRef(null);
205+
const boxXRef = useRef<number | null>(null);
206+
const boxYRef = useRef<number | null>(null);
186207

187208
const statusDecorator = useMemo(() => {
188209
if (!status || !showStatusDecorator) {
@@ -258,6 +279,8 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
258279

259280
const nodeLabelPosition = labelPosition || element.getLabelPosition();
260281
const scale = element.getGraph().getScale();
282+
const detailsLevel = element.getGraph().getDetailsLevel();
283+
const isLowDetailsLevel = detailsLevel === ScaleDetailsLevel.low;
261284

262285
const animationRef = useRef<number>(null);
263286
const scaleGoal = useRef<number>(1);
@@ -325,6 +348,12 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
325348
return { translateX, translateY };
326349
}, [element, nodeScale, scaleNode]);
327350

351+
const box = element.getBounds();
352+
if (!showDragGhostResolved || !dragging || boxXRef.current === null || boxYRef.current === null) {
353+
boxXRef.current = box.x;
354+
boxYRef.current = box.y;
355+
}
356+
328357
const renderLabel = () => {
329358
if (!showLabel || !(label || element.getLabel())) {
330359
return null;
@@ -398,28 +427,55 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
398427
};
399428

400429
return (
401-
<g
402-
className={groupClassName}
403-
transform={`${scaleNode ? `translate(${translateX}, ${translateY})` : ''} scale(${nodeScale})`}
404-
>
405-
<NodeShadows />
406-
<g ref={refs} onClick={onSelect} onContextMenu={onContextMenu}>
407-
{ShapeComponent && (
408-
<ShapeComponent
409-
className={backgroundClassName}
410-
element={element}
411-
width={width}
412-
height={height}
413-
dndDropRef={dndDropRef}
414-
filter={filter}
415-
/>
416-
)}
417-
{renderLabel()}
418-
{children}
430+
<>
431+
{dragging && showDragGhostResolved && (
432+
<g
433+
className={css(groupClassName, styles.modifiers.dragGhost)}
434+
transform={`translate(${boxXRef.current - box.x}, ${boxYRef.current - box.y}) ${
435+
isLowDetailsLevel ? '' : `scale(${DRAG_GHOST_SCALE})`
436+
}`}
437+
>
438+
<NodeShadows />
439+
<g>
440+
{ShapeComponent && (
441+
<ShapeComponent
442+
className={backgroundClassName}
443+
element={element}
444+
width={width}
445+
height={height}
446+
filter={filter}
447+
/>
448+
)}
449+
{!isLowDetailsLevel && renderLabel()}
450+
{!isLowDetailsLevel && children}
451+
</g>
452+
{statusDecorator}
453+
{attachments}
454+
</g>
455+
)}
456+
<g
457+
className={groupClassName}
458+
transform={`${scaleNode ? `translate(${translateX}, ${translateY})` : ''} scale(${nodeScale})`}
459+
>
460+
<NodeShadows />
461+
<g ref={refs} onClick={onSelect} onContextMenu={onContextMenu}>
462+
{ShapeComponent && (
463+
<ShapeComponent
464+
className={backgroundClassName}
465+
element={element}
466+
width={width}
467+
height={height}
468+
dndDropRef={dndDropRef}
469+
filter={filter}
470+
/>
471+
)}
472+
{renderLabel()}
473+
{children}
474+
</g>
475+
{statusDecorator}
476+
{attachments}
419477
</g>
420-
{statusDecorator}
421-
{attachments}
422-
</g>
478+
</>
423479
);
424480
}
425481
);

packages/module/src/css/topology-components.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
--pf-topology__node__background--Stroke: var(--pf-t--global--border--color--default);
1414
--pf-topology__node__background--StrokeWidth: var(--pf-t--global--border--width--regular);
1515
--pf-topology__node--m-dragging--background--StrokeWidth: var(--pf-t--global--border--width--regular);
16+
--pf-topology__node--m-drag-ghost--Opacity: 0.6;
1617

1718
/* Status changes */
1819
--pf-topology__node--m-disabled--Background--Fill: var(--pf-t--global--background--color--disabled--default);
@@ -318,6 +319,10 @@
318319
cursor: grab;
319320
}
320321

322+
.pf-topology__node.pf-m-drag-ghost {
323+
opacity: var(--pf-topology__node--m-drag-ghost--Opacity);
324+
}
325+
321326
.pf-topology__node.pf-m-selected .pf-topology__node__background {
322327
stroke-width: var(--pf-topology__node--m-selected__background--StrokeWidth);
323328
--pf-topology__node__background--Stroke: var(--pf-topology__node--m-selected--Background--Stroke);

0 commit comments

Comments
 (0)