Skip to content

Commit 7bf4e6b

Browse files
committed
feat: show ghost node during node drag
1 parent 661caa3 commit 7bf4e6b

File tree

6 files changed

+112
-4
lines changed

6 files changed

+112
-4
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/nodes/DefaultNode.tsx

Lines changed: 69 additions & 3 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,19 @@ 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 (previous behavior). Can also be set via
137+
* node model data: `data: { showDragGhost: true }`.
138+
*/
139+
showDragGhost?: boolean;
125140
}
126141

127142
const SCALE_UP_TIME = 200;
128143

144+
/** Scale factor for the drag ghost when the graph is not at low details level (visual “preview” size). */
145+
const DRAG_GHOST_SCALE = 0.7;
146+
129147
type DefaultNodeInnerProps = Omit<DefaultNodeProps, 'element'> & { element: Node };
130148

131149
const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observer(
@@ -172,8 +190,11 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
172190
onContextMenu,
173191
contextMenuOpen,
174192
raiseLabelOnHover = true,
175-
hideContextMenuKebab
193+
hideContextMenuKebab,
194+
showDragGhost
176195
}) => {
196+
const showDragGhostResolved =
197+
showDragGhost ?? (element.getData() as { showDragGhost?: boolean } | undefined)?.showDragGhost ?? false;
177198
const [nodeHovered, hoverRef] = useHover();
178199
const [labelHovered, labelRef] = useHover();
179200
const hovered = nodeHovered || labelHovered;
@@ -183,6 +204,8 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
183204
const isHover = hover !== undefined ? hover : hovered;
184205
const [nodeScale, setNodeScale] = useState<number>(1);
185206
const decoratorRef = useRef(null);
207+
const boxXRef = useRef<number | null>(null);
208+
const boxYRef = useRef<number | null>(null);
186209

187210
const statusDecorator = useMemo(() => {
188211
if (!status || !showStatusDecorator) {
@@ -258,6 +281,8 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
258281

259282
const nodeLabelPosition = labelPosition || element.getLabelPosition();
260283
const scale = element.getGraph().getScale();
284+
const detailsLevel = element.getGraph().getDetailsLevel();
285+
const isLowDetailsLevel = detailsLevel === ScaleDetailsLevel.low;
261286

262287
const animationRef = useRef<number>(null);
263288
const scaleGoal = useRef<number>(1);
@@ -325,6 +350,12 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
325350
return { translateX, translateY };
326351
}, [element, nodeScale, scaleNode]);
327352

353+
const box = element.getBounds();
354+
if (!showDragGhostResolved || !dragging || !boxXRef.current || !boxYRef.current) {
355+
boxXRef.current = box.x;
356+
boxYRef.current = box.y;
357+
}
358+
328359
const renderLabel = () => {
329360
if (!showLabel || !(label || element.getLabel())) {
330361
return null;
@@ -397,7 +428,7 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
397428
return nodeLabel;
398429
};
399430

400-
return (
431+
const mainNode = (
401432
<g
402433
className={groupClassName}
403434
transform={`${scaleNode ? `translate(${translateX}, ${translateY})` : ''} scale(${nodeScale})`}
@@ -421,6 +452,41 @@ const DefaultNodeInner: React.FunctionComponent<DefaultNodeInnerProps> = observe
421452
{attachments}
422453
</g>
423454
);
455+
456+
if (!showDragGhostResolved) {
457+
return mainNode;
458+
}
459+
460+
return (
461+
<>
462+
{dragging && (
463+
<g
464+
className={groupClassName}
465+
transform={`translate(${boxXRef.current - box.x}, ${boxYRef.current - box.y}) ${
466+
isLowDetailsLevel ? '' : `scale(${DRAG_GHOST_SCALE})`
467+
}`}
468+
>
469+
<NodeShadows />
470+
<g>
471+
{ShapeComponent && (
472+
<ShapeComponent
473+
className={backgroundClassName}
474+
element={element}
475+
width={width}
476+
height={height}
477+
filter={filter}
478+
/>
479+
)}
480+
{!isLowDetailsLevel && renderLabel()}
481+
{!isLowDetailsLevel && children}
482+
</g>
483+
{statusDecorator}
484+
{attachments}
485+
</g>
486+
)}
487+
{mainNode}
488+
</>
489+
);
424490
}
425491
);
426492

0 commit comments

Comments
 (0)