diff --git a/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx
index 3c3f70e7..8d48e3ac 100644
--- a/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx
+++ b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx
@@ -132,6 +132,32 @@ export const NodeDecoratorStyles = withTopologySetup(() => {
return null;
});
+export const NodeShowDragGhostStyles = withTopologySetup(() => {
+ useComponentFactory(defaultComponentFactory);
+ useComponentFactory(stylesComponentFactory);
+ const nodes: NodeModel[] = createGroupNodes();
+ const nodes2: NodeModel[] = createGroupNodes('2', 600);
+
+ nodes.forEach((n) => (n.data.showDecorators = true));
+ nodes.forEach((n) => (n.data.labelPosition = LabelPosition.bottom));
+ nodes.forEach((n) => (n.data.showDragGhost = true));
+ nodes2.forEach((n) => (n.data.showDecorators = true));
+ nodes2.forEach((n) => (n.data.showDragGhost = true));
+ useModel(
+ useMemo(
+ (): Model => ({
+ graph: {
+ id: 'g1',
+ type: 'graph'
+ },
+ nodes: [...nodes, ...nodes2]
+ }),
+ [nodes, nodes2]
+ )
+ );
+ return null;
+});
+
export const NodeLabelStyles = withTopologySetup(() => {
useComponentFactory(defaultComponentFactory);
useComponentFactory(stylesComponentFactory);
@@ -928,6 +954,9 @@ export const StyleNodes: React.FunctionComponent = () => {
Decorators}>
+ Show Drag Ghost While Dragging}>
+
+
);
diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx
index 7badd985..936fcc61 100644
--- a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx
+++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx
@@ -15,7 +15,8 @@ export class DemoModel {
badges: false,
icons: false,
contextMenus: false,
- hulledOutline: true
+ hulledOutline: true,
+ showDragGhost: false
};
protected edgeOptionsP: GeneratorEdgeOptions = {
showStyles: false,
diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx
index 99ab8e34..aa834ea6 100644
--- a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx
+++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx
@@ -194,6 +194,7 @@ const DemoNode: React.FunctionComponent = observer(
options.showDecorators &&
renderDecorators(options, nodeElement, rest.getShapeDecoratorCenter)
}
+ showDragGhost={options.showDragGhost}
>
{(focused || detailsLevel !== ScaleDetailsLevel.low) && renderIcon(data, nodeElement)}
diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx
index 57d436ce..26aead22 100644
--- a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx
+++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx
@@ -146,6 +146,16 @@ const OptionsContextBar: React.FC = observer(() => {
>
Rectangle Groups
+
+ options.setNodeOptions({ ...options.nodeOptions, showDragGhost: !options.nodeOptions.showDragGhost })
+ }
+ >
+ Show Drag Ghost
+
);
diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts
index 880462d4..c35daac0 100644
--- a/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts
+++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts
@@ -91,6 +91,7 @@ export interface GeneratorNodeOptions {
contextMenus?: boolean;
hideKebabMenu?: boolean;
hulledOutline?: boolean;
+ showDragGhost?: boolean;
}
export interface GeneratorEdgeOptions {
diff --git a/packages/module/src/components/edges/DefaultEdge.tsx b/packages/module/src/components/edges/DefaultEdge.tsx
index 8dd87482..c1f57831 100644
--- a/packages/module/src/components/edges/DefaultEdge.tsx
+++ b/packages/module/src/components/edges/DefaultEdge.tsx
@@ -84,8 +84,7 @@ interface DefaultEdgeProps {
/**
* 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 }`.
+ * during drag as before.
*/
freezeEdgeDuringNodeDrag?: boolean;
}
@@ -124,10 +123,7 @@ const DefaultEdgeInner: React.FunctionComponent = observe
onContextMenu,
freezeEdgeDuringNodeDrag
}) => {
- const freezeDuringDrag =
- freezeEdgeDuringNodeDrag ??
- (element.getData() as { freezeEdgeDuringNodeDrag?: boolean } | undefined)?.freezeEdgeDuringNodeDrag ??
- false;
+ const freezeDuringDrag = freezeEdgeDuringNodeDrag ?? false;
const [hover, hoverRef] = useHover();
const edgeRef = useCombineRefs(hoverRef, dndDropRef);
diff --git a/packages/module/src/components/nodes/DefaultNode.tsx b/packages/module/src/components/nodes/DefaultNode.tsx
index 4578d352..725acabe 100644
--- a/packages/module/src/components/nodes/DefaultNode.tsx
+++ b/packages/module/src/components/nodes/DefaultNode.tsx
@@ -6,7 +6,16 @@ import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
import styles from '../../css/topology-components';
-import { BadgeLocation, GraphElement, isNode, LabelPosition, Node, NodeStatus, TopologyQuadrant } from '../../types';
+import {
+ BadgeLocation,
+ GraphElement,
+ isNode,
+ LabelPosition,
+ Node,
+ NodeStatus,
+ ScaleDetailsLevel,
+ TopologyQuadrant
+} from '../../types';
import { ConnectDragSource, ConnectDropTarget, OnSelect, WithDndDragProps } from '../../behavior';
import Decorator from '../decorators/Decorator';
import { Layer } from '../layers';
@@ -122,10 +131,18 @@ interface DefaultNodeProps {
raiseLabelOnHover?: boolean; // TODO: Update default to be false, assume demo code will be followed
/** Hide context menu kebab for the node */
hideContextMenuKebab?: boolean;
+ /**
+ * When true, a non-interactive copy of the node is drawn at the pre-drag position while dragging.
+ * When false or omitted, only the live node is shown (default behavior).
+ */
+ showDragGhost?: boolean;
}
const SCALE_UP_TIME = 200;
+/** Scale factor for the drag ghost when the graph is not at low details level. */
+const DRAG_GHOST_SCALE = 0.7;
+
type DefaultNodeInnerProps = Omit & { element: Node };
const DefaultNodeInner: React.FunctionComponent = observer(
@@ -172,8 +189,10 @@ const DefaultNodeInner: React.FunctionComponent = observe
onContextMenu,
contextMenuOpen,
raiseLabelOnHover = true,
- hideContextMenuKebab
+ hideContextMenuKebab,
+ showDragGhost
}) => {
+ const showDragGhostResolved = showDragGhost ?? false;
const [nodeHovered, hoverRef] = useHover();
const [labelHovered, labelRef] = useHover();
const hovered = nodeHovered || labelHovered;
@@ -183,6 +202,8 @@ const DefaultNodeInner: React.FunctionComponent = observe
const isHover = hover !== undefined ? hover : hovered;
const [nodeScale, setNodeScale] = useState(1);
const decoratorRef = useRef(null);
+ const boxXRef = useRef(null);
+ const boxYRef = useRef(null);
const statusDecorator = useMemo(() => {
if (!status || !showStatusDecorator) {
@@ -258,6 +279,8 @@ const DefaultNodeInner: React.FunctionComponent = observe
const nodeLabelPosition = labelPosition || element.getLabelPosition();
const scale = element.getGraph().getScale();
+ const detailsLevel = element.getGraph().getDetailsLevel();
+ const isLowDetailsLevel = detailsLevel === ScaleDetailsLevel.low;
const animationRef = useRef(null);
const scaleGoal = useRef(1);
@@ -325,6 +348,12 @@ const DefaultNodeInner: React.FunctionComponent = observe
return { translateX, translateY };
}, [element, nodeScale, scaleNode]);
+ const box = element.getBounds();
+ if (!showDragGhostResolved || !dragging || boxXRef.current === null || boxYRef.current === null) {
+ boxXRef.current = box.x;
+ boxYRef.current = box.y;
+ }
+
const renderLabel = () => {
if (!showLabel || !(label || element.getLabel())) {
return null;
@@ -398,28 +427,55 @@ const DefaultNodeInner: React.FunctionComponent = observe
};
return (
-
-
-
- {ShapeComponent && (
-
- )}
- {renderLabel()}
- {children}
+ <>
+ {dragging && showDragGhostResolved && (
+
+
+
+ {ShapeComponent && (
+
+ )}
+ {!isLowDetailsLevel && renderLabel()}
+ {!isLowDetailsLevel && children}
+
+ {statusDecorator}
+ {attachments}
+
+ )}
+
+
+
+ {ShapeComponent && (
+
+ )}
+ {renderLabel()}
+ {children}
+
+ {statusDecorator}
+ {attachments}
- {statusDecorator}
- {attachments}
-
+ >
);
}
);
diff --git a/packages/module/src/css/topology-components.css b/packages/module/src/css/topology-components.css
index 398a4578..fad1b3b9 100644
--- a/packages/module/src/css/topology-components.css
+++ b/packages/module/src/css/topology-components.css
@@ -13,6 +13,7 @@
--pf-topology__node__background--Stroke: var(--pf-t--global--border--color--default);
--pf-topology__node__background--StrokeWidth: var(--pf-t--global--border--width--regular);
--pf-topology__node--m-dragging--background--StrokeWidth: var(--pf-t--global--border--width--regular);
+ --pf-topology__node--m-drag-ghost--Opacity: 0.6;
/* Status changes */
--pf-topology__node--m-disabled--Background--Fill: var(--pf-t--global--background--color--disabled--default);
@@ -318,6 +319,10 @@
cursor: grab;
}
+.pf-topology__node.pf-m-drag-ghost {
+ opacity: var(--pf-topology__node--m-drag-ghost--Opacity);
+}
+
.pf-topology__node.pf-m-selected .pf-topology__node__background {
stroke-width: var(--pf-topology__node--m-selected__background--StrokeWidth);
--pf-topology__node__background--Stroke: var(--pf-topology__node--m-selected--Background--Stroke);