Skip to content

Context Menu

This example demonstrates how to implement a context menu that adapts its options based on the context—showing node-specific actions when a node is right-clicked, and diagram-wide actions otherwise.

The context menu appears at the cursor position when the user right-clicks on a node or the diagram. The example provides relevant actions such as copy, paste, delete, and select all.

  • Context Awareness: Menu options change depending on whether a node or the diagram background is right-clicked.
  • Positioning: The menu appears at the exact cursor location using viewport coordinates.
  • Integration: Uses Angular signals and services for reactive state management.

The ContextMenuService manages the menu’s visibility, position, and context (node or diagram):

@Injectable()
export class ContextMenuService {
readonly visibility = signal(false);
readonly menuPosition = signal({ x: 0, y: 0 });
readonly nodeContext = signal(false);
showMenu({ x, y }: Point): void {
this.nodeContext.set(true);
this.menuPosition.set({ x, y });
this.visibility.set(true);
}
showDiagramMenu({ x, y }: Point): void {
this.nodeContext.set(false);
this.menuPosition.set({ x, y });
this.visibility.set(true);
}
hideMenu(): void {
this.visibility.set(false);
}
}

The menu component displays options based on the context and stores information about menu positioning:

<div #menu class="menu" [style.top.px]="menuPosition().y" [style.left.px]="menuPosition().x" [class.show]="showMenu()">
<ul>
@if (nodeContext()) {
<li (click)="onCopy()">Copy</li>
<li (click)="onDelete()">Delete</li>
} @else {
<li (click)="onSelectAll()">Select All</li>
<li (click)="onPaste($event)">Paste</li>
}
</ul>
</div>

Nodes handle right-click events to show the context menu and select the node. The menu position is calculated using viewport coordinates:

onRightClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.node()) {
// Additionally selects the node on right click
this.selectionService.select([this.node().id]);
const cursorPosition = this.viewportService.clientToFlowViewportPosition({ x: event.clientX, y: event.clientY });
this.contextMenuService.showMenu(cursorPosition);
}
}

Right-clicking the diagram background shows the diagram-wide menu, also using viewport coordinates:

onDiagramRightClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const cursorPosition = this.viewportService.clientToFlowViewportPosition({ x: event.clientX, y: event.clientY });
this.contextMenuService.showDiagramMenu(cursorPosition);
}
  • Copy: Copies the selected node(s) to the clipboard.
  • Paste: Pastes copied nodes at the cursor position.
  • Delete: Removes the selected node(s).
  • Select All: Selects all nodes in the diagram.
import '@angular/compiler';
import { Component, inject } from '@angular/core';
import {
initializeModel,
NgDiagramComponent,
NgDiagramNodeTemplateMap,
NgDiagramViewportService,
type NgDiagramConfig,
} from 'ng-diagram';
import { MenuComponent } from './menu/menu.component';
import { ContextMenuService } from './menu/menu.service';
import { NodeComponent } from './node/node.component';
@Component({
selector: 'context-menu-example',
imports: [NgDiagramComponent, MenuComponent],
template: `
<div (contextmenu)="onDiagramRightClick($event)">
<div class="not-content diagram">
<ng-diagram
[model]="model"
[config]="config"
[nodeTemplateMap]="nodeTemplateMap"
/>
</div>
<menu></menu>
</div>
`,
styleUrl: './context-menu-example.component.scss',
providers: [ContextMenuService, NgDiagramViewportService],
})
export class ContextMenuExampleComponent {
private contextMenuService = inject(ContextMenuService);
private readonly viewportService = inject(NgDiagramViewportService);
nodeTemplateMap = new NgDiagramNodeTemplateMap([
['customNodeType', NodeComponent],
]);
config = {
zoom: {
max: 3,
},
} satisfies NgDiagramConfig;
model = initializeModel({
nodes: [
{
id: '1',
position: { x: 100, y: 100 },
type: 'customNodeType',
data: {
name: 'Custom Node',
},
},
],
edges: [],
metadata: { viewport: { x: 0, y: 0, scale: 1 } },
});
onDiagramRightClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const cursorPosition = this.viewportService.clientToFlowViewportPosition({
x: event.clientX,
y: event.clientY,
});
this.contextMenuService.showDiagramMenu(cursorPosition);
}
}