@@ -6,7 +6,16 @@ import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle
66import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon' ;
77import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon' ;
88import 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' ;
1019import { ConnectDragSource , ConnectDropTarget , OnSelect , WithDndDragProps } from '../../behavior' ;
1120import Decorator from '../decorators/Decorator' ;
1221import { 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
127141const 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+
129146type DefaultNodeInnerProps = Omit < DefaultNodeProps , 'element' > & { element : Node } ;
130147
131148const 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) ;
0 commit comments