src/app/graphs/component-context-menu/component-context-menu.component.ts
This component manages the component context menu as well as its content
styleUrls | component-context-menu.component.scss |
templateUrl | ./component-context-menu.component.html |
Properties |
|
Methods |
constructor(data: ComponentContextMenuData, changeDetector: ChangeDetectorRef)
|
|||||||||
Parameters :
|
close |
close()
|
Close the context menu
Returns :
void
|
updatePosition | ||||||||||||
updatePosition(x: number, y: number)
|
||||||||||||
Update the position of the context menu
Parameters :
Returns :
void
|
Public data |
Type : ComponentContextMenuData
|
Decorators :
@Inject(COMPONENT_CONTEXT_MENU_DATA)
|
height |
Default value : ComponentContextMenuComponent.LAST_HEIGHT
|
Current height of the dialog |
nodeDetailsReady |
Type : boolean
|
True if the node details component is loaded |
width |
Default value : ComponentContextMenuComponent.LAST_WIDTH
|
Current width of the dialog |
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
Inject,
Injectable,
InjectionToken,
Injector,
OnDestroy,
ViewChild
} from '@angular/core';
import {ConnectedPosition, Overlay, OverlayRef} from '@angular/cdk/overlay';
import {ComponentPortal, PortalInjector} from '@angular/cdk/portal';
import {IssueGraphComponent} from '@app/graphs/issue-graph/issue-graph.component';
import {NodeDetailsComponent, NodeDetailsType, NodeUpdatedCallbackFn} from '@app/node-details/node-details.component';
/**
* Interface specifying the data required for the component context menu.
* Note that this should not be used directly, instead the ComponentContextMenuService should be used to open a context
* menu.
*/
interface ComponentContextMenuData {
/** Reference to the overlay used to display the context menu */
overlayRef: OverlayRef;
/** The position of the overlay */
position: ConnectedPosition;
/** The project id string */
projectId: string;
/** The node id string */
nodeId: string;
/** The type of node, either interface or component. Controls the content shown in the context menu */
type: NodeDetailsType;
/** A reference to the issue graph */
graph: IssueGraphComponent;
}
const COMPONENT_CONTEXT_MENU_DATA = new InjectionToken<ComponentContextMenuData>('COMPONENT_CONTEXT_MENU_DATA');
/**
* Use this service to create a {@link ComponentContextMenuComponent}.
*/
@Injectable({providedIn: 'root'})
export class ComponentContextMenuService {
constructor(private overlay: Overlay, private injector: Injector) {}
/**
* Open a new component context menu
* @param parent The parent of the context menu
* @param x The X position relative to the top left corner of the parent
* @param y The Y position relative to the top left corner of the parent
* @param projectID The id of the project the node belongs to
* @param nodeID The id of the node
* @param nodeType The type of the node
* @param issueGraph A reference to the issue graph
* @return A reference to the context menu
*/
open(
parent: Element,
x: number,
y: number,
projectID: string,
nodeID: string,
nodeType: NodeDetailsType,
issueGraph: IssueGraphComponent
): ComponentContextMenuComponent {
const position = this.overlay.position().flexibleConnectedTo(parent);
const pos: ConnectedPosition = {
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
offsetX: x,
offsetY: y
};
position.withPositions([pos]);
const ref = this.overlay.create({
minWidth: 400,
minHeight: 200,
positionStrategy: position
});
const map = new WeakMap();
map.set(COMPONENT_CONTEXT_MENU_DATA, {
overlayRef: ref,
position: pos,
projectId: projectID,
nodeId: nodeID,
type: nodeType,
graph: issueGraph
});
const injector = new PortalInjector(this.injector, map);
return ref.attach(new ComponentPortal(ComponentContextMenuComponent, null, injector)).instance;
}
}
/**
* This component manages the component context menu as well as its content
*/
@Component({
styleUrls: ['component-context-menu.component.scss'],
templateUrl: './component-context-menu.component.html'
})
export class ComponentContextMenuComponent implements AfterViewInit, OnDestroy {
/** @ignore */
private static MIN_WIDTH = 700;
/** @ignore */
private static MIN_HEIGHT = 400;
/** @ignore */
private static LAST_WIDTH = ComponentContextMenuComponent.MIN_WIDTH;
/** @ignore */
private static LAST_HEIGHT = ComponentContextMenuComponent.MIN_HEIGHT;
/** @ignore */
private resize = false;
/** Current width of the dialog */
width = ComponentContextMenuComponent.LAST_WIDTH;
/** Current height of the dialog */
height = ComponentContextMenuComponent.LAST_HEIGHT;
/** True if the node details component is loaded */
nodeDetailsReady: boolean;
/** @ignore */
@ViewChild('frame') frame: ElementRef;
/** @ignore */
@ViewChild('resizeCorner') set resizeCorner(content: ElementRef) {
if (content) {
content.nativeElement.addEventListener('mousedown', () => (this.resize = true));
}
}
/** @ignore */
@ViewChild(NodeDetailsComponent) nodeDetails: NodeDetailsComponent;
constructor(@Inject(COMPONENT_CONTEXT_MENU_DATA) public data: ComponentContextMenuData, private changeDetector: ChangeDetectorRef) {}
ngAfterViewInit() {
this.frame.nativeElement.style.minWidth = ComponentContextMenuComponent.MIN_WIDTH + 'px';
this.frame.nativeElement.style.minHeight = ComponentContextMenuComponent.MIN_HEIGHT + 'px';
this.nodeDetailsReady = true;
this.changeDetector.detectChanges();
}
ngOnDestroy(): void {
// TODO: Save in local storage?
ComponentContextMenuComponent.LAST_WIDTH = this.width;
ComponentContextMenuComponent.LAST_HEIGHT = this.height;
}
/** @ignore */
detailsCallback: NodeUpdatedCallbackFn = (nodeDeleted: boolean): void => {
this.data.graph.reload();
if (nodeDeleted) {
this.close();
}
};
/**
* Update the position of the context menu
* @param x The X offset of the top left menu corner relative to the top left corner of the parent
* @param y The Y offset of the top left menu corner relative to the top left corner of the parent
*/
updatePosition(x: number, y: number): void {
this.data.position.offsetX = x;
this.data.position.offsetY = y;
this.data.overlayRef.getConfig().positionStrategy.apply();
}
/**
* Close the context menu
*/
close(): void {
this.data.overlayRef.dispose();
}
/** @ignore */
@HostListener('window:mouseup')
private onMouseUp() {
this.resize = false;
}
/** @ignore */
@HostListener('window:mousemove', ['$event'])
private onMouseMove(event: MouseEvent) {
if (!this.resize) {
return;
}
this.width = Math.max(this.width + event.movementX, ComponentContextMenuComponent.MIN_WIDTH);
this.height = Math.max(this.height + event.movementY, ComponentContextMenuComponent.MIN_HEIGHT);
}
}
<div #frame class="frame" [style.width.px]="width" [style.height.px]="height">
<div style="height: calc(100% - 20px); overflow: hidden">
<div class="node-title" *ngIf="nodeDetailsReady && nodeDetails.getNodeName()">
<h1 style="overflow: hidden; text-overflow: ellipsis">{{ this.nodeDetails.getNodeName() }}</h1>
</div>
<div class="container">
<app-node-details
[projectId]="data.projectId"
[nodeId]="data.nodeId"
[nodeType]="data.type"
[callback]="detailsCallback"
></app-node-details>
</div>
</div>
<div style="width: 100%; height: 20px; display: inline-block">
<span style="font-size: 0.8em">{{ this.data.nodeId }}</span>
<span #resizeCorner style="position: absolute; bottom: 0; right: 4px; user-select: none">
<mat-icon svgIcon="resize-corner" style="width: 10px; height: 10px; cursor: nwse-resize"></mat-icon>
</span>
</div>
</div>
component-context-menu.component.scss
@import "src/styles/variables";
.frame {
padding: 24px;
background-color: $background-controls;
border-radius: 4px;
box-shadow: 0 11px 15px -7px rgb(0 0 0 / 20%),
0 24px 38px 3px rgb(0 0 0 / 14%), 0 9px 46px 8px rgb(0 0 0 / 12%);
}
.node-title {
width: 100%;
text-align: center;
margin: 0 0 20px;
}
.container {
height: calc(100% - 50px);
overflow: auto;
}