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);