Context menu for list of items

This commit is contained in:
Piotr Pekala 2019-01-16 04:03:26 -08:00
parent 4fa82bc81d
commit ed59d2ba02
15 changed files with 188 additions and 70 deletions

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { SelectionManager } from '../../managers/selection-manager'; import { SelectionManager } from '../../managers/selection-manager';
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
@ -12,7 +12,10 @@ export class SelectionSelectComponent implements OnInit, OnDestroy {
private onSelected: Subscription; private onSelected: Subscription;
private onUnselected: Subscription; private onUnselected: Subscription;
constructor(private selectionManager: SelectionManager, private mapChangeDetectorRef: MapChangeDetectorRef) {} constructor(
private selectionManager: SelectionManager,
private mapChangeDetectorRef: MapChangeDetectorRef
) {}
ngOnInit() { ngOnInit() {
this.onSelected = this.selectionManager.selected.subscribe(() => { this.onSelected = this.selectionManager.selected.subscribe(() => {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { mouse, select } from 'd3-selection'; import { mouse, select, event } from 'd3-selection';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { SVGSelection } from '../models/types'; import { SVGSelection } from '../models/types';
@ -12,20 +12,39 @@ export class SelectionTool {
static readonly SELECTABLE_CLASS = '.selectable'; static readonly SELECTABLE_CLASS = '.selectable';
public rectangleSelected = new Subject<Rectangle>(); public rectangleSelected = new Subject<Rectangle>();
public contextMenuOpened = new EventEmitter<any>();
private path; private path;
private enabled = false; private enabled = false;
public constructor(private context: Context, private selectionEventSource: SelectionEventSource) {} public constructor(
private context: Context,
private selectionEventSource: SelectionEventSource
) {}
public disableContextMenu(){
}
public setEnabled(enabled) { public setEnabled(enabled) {
this.enabled = enabled; this.enabled = enabled;
this.contextMenuOpened.emit(true);
} }
private activate(selection) { private activate(selection) {
const self = this; const self = this;
selection.on('mousedown', function() { selection.on('mousedown', function() {
// prevent deselection on right click
if (event.button == 2) {
selection.on('contextmenu', () => {
event.preventDefault();
});
self.contextMenuOpened.emit(event);
return;
}
const subject = select(window); const subject = select(window);
const parent = this.parentElement; const parent = this.parentElement;

View File

@ -13,8 +13,8 @@ import { DrawingService } from '../../../../../services/drawing.service';
}) })
export class MoveLayerDownActionComponent implements OnInit { export class MoveLayerDownActionComponent implements OnInit {
@Input() server: Server; @Input() server: Server;
@Input() node: Node; @Input() nodes: Node[];
@Input() drawing: Drawing; @Input() drawings: Drawing[];
constructor( constructor(
private nodesDataSource: NodesDataSource, private nodesDataSource: NodesDataSource,
@ -26,16 +26,18 @@ export class MoveLayerDownActionComponent implements OnInit {
ngOnInit() {} ngOnInit() {}
moveLayerDown() { moveLayerDown() {
if (this.node) { this.nodes.forEach((node) => {
this.node.z--; node.z--;
this.nodesDataSource.update(this.node); this.nodesDataSource.update(node);
this.nodeService.update(this.server, this.node).subscribe((node: Node) => {}); this.nodeService.update(this.server, node).subscribe((node: Node) => {});
} else if (this.drawing) { });
this.drawing.z--;
this.drawingsDataSource.update(this.drawing);
this.drawingService.update(this.server, this.drawing).subscribe((drawing: Drawing) => {}); this.drawings.forEach((drawing) => {
} drawing.z--;
this.drawingsDataSource.update(drawing);
this.drawingService.update(this.server, drawing).subscribe((drawing: Drawing) => {});
});
} }
} }

View File

@ -13,8 +13,8 @@ import { DrawingService } from '../../../../../services/drawing.service';
}) })
export class MoveLayerUpActionComponent implements OnInit { export class MoveLayerUpActionComponent implements OnInit {
@Input() server: Server; @Input() server: Server;
@Input() node: Node; @Input() nodes: Node[];
@Input() drawing: Drawing; @Input() drawings: Drawing[];
constructor( constructor(
private nodesDataSource: NodesDataSource, private nodesDataSource: NodesDataSource,
@ -26,16 +26,18 @@ export class MoveLayerUpActionComponent implements OnInit {
ngOnInit() {} ngOnInit() {}
moveLayerUp() { moveLayerUp() {
if (this.node) { this.nodes.forEach((node) => {
this.node.z++; node.z++;
this.nodesDataSource.update(this.node); this.nodesDataSource.update(node);
this.nodeService.update(this.server, this.node).subscribe((node: Node) => {}); this.nodeService.update(this.server, node).subscribe((node: Node) => {});
} else if (this.drawing) { });
this.drawing.z++;
this.drawingsDataSource.update(this.drawing);
this.drawingService.update(this.server, this.drawing).subscribe((drawing: Drawing) => {}); this.drawings.forEach((drawing) => {
} drawing.z++;
this.drawingsDataSource.update(drawing);
this.drawingService.update(this.server, drawing).subscribe((drawing: Drawing) => {});
});
} }
} }

View File

@ -1,4 +1,4 @@
<button mat-menu-item *ngIf="node.status == 'stopped'" (click)="startNode()"> <button mat-menu-item *ngIf="isNodeWithStoppedStatus" (click)="startNodes()">
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
<span>Start</span> <span>Start</span>
</button> </button>

View File

@ -9,13 +9,22 @@ import { Node } from '../../../../../cartography/models/node';
}) })
export class StartNodeActionComponent implements OnInit { export class StartNodeActionComponent implements OnInit {
@Input() server: Server; @Input() server: Server;
@Input() node: Node; @Input() nodes: Node[];
private isNodeWithStoppedStatus: boolean;
constructor(private nodeService: NodeService) {} constructor(private nodeService: NodeService) {}
ngOnInit() {} ngOnInit() {
this.nodes.forEach((node) => {
if (node.status === 'stopped') {
this.isNodeWithStoppedStatus = true;
}
});
}
startNode() { startNodes() {
this.nodeService.start(this.server, this.node).subscribe((n: Node) => {}); this.nodes.forEach((node) => {
this.nodeService.start(this.server, node).subscribe((n: Node) => {});
});
} }
} }

View File

@ -1,4 +1,4 @@
<button mat-menu-item *ngIf="node.status == 'started'" (click)="stopNode()"> <button mat-menu-item *ngIf="isNodeWithStartedStatus" (click)="stopNodes()">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
<span>Stop</span> <span>Stop</span>
</button> </button>

View File

@ -9,13 +9,22 @@ import { Node } from '../../../../../cartography/models/node';
}) })
export class StopNodeActionComponent implements OnInit { export class StopNodeActionComponent implements OnInit {
@Input() server: Server; @Input() server: Server;
@Input() node: Node; @Input() nodes: Node[];
private isNodeWithStartedStatus: boolean;
constructor(private nodeService: NodeService) {} constructor(private nodeService: NodeService) {}
ngOnInit() {} ngOnInit() {
this.nodes.forEach((node) => {
if (node.status === 'started') {
this.isNodeWithStartedStatus = true;
}
});
}
stopNode() { stopNodes() {
this.nodeService.stop(this.server, this.node).subscribe((n: Node) => {}); this.nodes.forEach((node) => {
this.nodeService.stop(this.server, node).subscribe((n: Node) => {});
});
} }
} }

View File

@ -1,31 +1,30 @@
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition" *ngIf="node || drawing"> <div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition">
<span [matMenuTriggerFor]="contextMenu"></span> <span [matMenuTriggerFor]="contextMenu"></span>
<mat-menu #contextMenu="matMenu" class="context-menu-items"> <mat-menu #contextMenu="matMenu" class="context-menu-items">
<app-start-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-start-node-action> <app-start-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-start-node-action>
<app-stop-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-stop-node-action> <app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
<app-edit-style-action <app-edit-style-action *ngIf="drawings.length===1 && !hasTextCapabilities"
*ngIf="hasDrawingCapabilities && !isTextElement"
[server]="server" [server]="server"
[project]="project" [project]="project"
[drawing]="drawing" [drawing]="drawings[0]"
></app-edit-style-action> ></app-edit-style-action>
<app-edit-text-action <app-edit-text-action
*ngIf="hasDrawingCapabilities && isTextElement" *ngIf="drawings.length===1 && hasTextCapabilities"
[server]="server" [server]="server"
[project]="project" [project]="project"
[drawing]="drawing" [drawing]="drawings[0]"
></app-edit-text-action> ></app-edit-text-action>
<app-move-layer-up-action <app-move-layer-up-action
*ngIf="!projectService.isReadOnly(project)" *ngIf="!projectService.isReadOnly(project)"
[server]="server" [server]="server"
[node]="node" [nodes]="nodes"
[drawing]="drawing" [drawings]="drawings"
></app-move-layer-up-action> ></app-move-layer-up-action>
<app-move-layer-down-action <app-move-layer-down-action
*ngIf="!projectService.isReadOnly(project)" *ngIf="!projectService.isReadOnly(project)"
[server]="server" [server]="server"
[node]="node" [nodes]="nodes"
[drawing]="drawing" [drawings]="drawings"
></app-move-layer-down-action> ></app-move-layer-down-action>
</mat-menu> </mat-menu>
</div> </div>

View File

@ -16,7 +16,10 @@ describe('ContextMenuComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [MatMenuModule, BrowserModule], imports: [MatMenuModule, BrowserModule],
providers: [{ provide: ChangeDetectorRef }, { provide: ProjectService, useClass: MockedProjectService }], providers: [
{ provide: ChangeDetectorRef },
{ provide: ProjectService, useClass: MockedProjectService }
],
declarations: [ContextMenuComponent], declarations: [ContextMenuComponent],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@ -62,4 +65,13 @@ describe('ContextMenuComponent', () => {
expect(spy.calls.any()).toBeTruthy(); expect(spy.calls.any()).toBeTruthy();
}); });
it('should reset capabilities while opening menu for list of elements', () => {
component.contextMenu = { openMenu() {} } as MatMenuTrigger;
var spy = spyOn<any>(component, 'resetCapabilities');
spyOn(component, 'setPosition').and.callFake(() => {});
component.openMenuForListOfElements([], [], [], 0, 0);
expect(spy.calls.any()).toBeTruthy();
});
}); });

View File

@ -7,6 +7,8 @@ import { Project } from '../../../models/project';
import { ProjectService } from '../../../services/project.service'; import { ProjectService } from '../../../services/project.service';
import { Drawing } from '../../../cartography/models/drawing'; import { Drawing } from '../../../cartography/models/drawing';
import { TextElement } from '../../../cartography/models/drawings/text-element'; import { TextElement } from '../../../cartography/models/drawings/text-element';
import { Label } from '../../../cartography/models/label';
@Component({ @Component({
selector: 'app-context-menu', selector: 'app-context-menu',
@ -21,11 +23,12 @@ export class ContextMenuComponent implements OnInit {
topPosition; topPosition;
leftPosition; leftPosition;
node: Node;
drawing: Drawing; drawings: Drawing[] = [];
private hasNodeCapabilities: boolean = false; nodes: Node[] = [];
private hasDrawingCapabilities: boolean = false; labels: Label[] = [];
private isTextElement: boolean = false;
private hasTextCapabilities: boolean = false;
constructor( constructor(
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
@ -43,32 +46,49 @@ export class ContextMenuComponent implements OnInit {
this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
} }
public openMenuForNode(node: Node, top: number, left: number) { public openMenuForDrawing(drawing: Drawing, top: number, left: number) {
this.resetCapabilities(); this.resetCapabilities();
this.hasNodeCapabilities = true; this.hasTextCapabilities = drawing.element instanceof TextElement;
this.node = node; this.drawings = [drawing];
this.setPosition(top, left); this.setPosition(top, left);
this.contextMenu.openMenu(); this.contextMenu.openMenu();
} }
public openMenuForDrawing(drawing: Drawing, top: number, left: number) { public openMenuForNode(node: Node, top: number, left: number) {
this.resetCapabilities(); this.resetCapabilities();
this.hasDrawingCapabilities = true;
this.isTextElement = drawing.element instanceof TextElement;
this.drawing = drawing; this.nodes = [node];
this.setPosition(top, left);
this.contextMenu.openMenu();
}
public openMenuForLabel(label: Label, top: number, left: number) {
this.resetCapabilities();
this.labels = [label];
this.setPosition(top, left);
this.contextMenu.openMenu();
}
public openMenuForListOfElements(drawings: Drawing[], nodes: Node[], labels: Label[], top: number, left: number) {
this.resetCapabilities();
this.drawings = drawings;
this.nodes = nodes;
this.labels = labels;
this.setPosition(top, left); this.setPosition(top, left);
this.contextMenu.openMenu(); this.contextMenu.openMenu();
} }
private resetCapabilities() { private resetCapabilities() {
this.node = null; this.drawings = [];
this.drawing = null; this.nodes = [];
this.hasDrawingCapabilities = false; this.labels = [];
this.hasNodeCapabilities = false; this.hasTextCapabilities = false;
this.isTextElement = false;
} }
} }

View File

@ -33,6 +33,9 @@ import { Node } from '../../cartography/models/node';
import { ToolsService } from '../../services/tools.service'; import { ToolsService } from '../../services/tools.service';
import { DrawingsWidget } from '../../cartography/widgets/drawings'; import { DrawingsWidget } from '../../cartography/widgets/drawings';
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter'; import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter';
import { SelectionManager } from '../../cartography/managers/selection-manager';
import { SelectionTool } from '../../cartography/tools/selection-tool';
export class MockedProgressService { export class MockedProgressService {
public activate() {} public activate() {}
@ -139,11 +142,14 @@ describe('ProjectMapComponent', () => {
{ provide: DrawingsWidget }, { provide: DrawingsWidget },
{ provide: MapNodeToNodeConverter }, { provide: MapNodeToNodeConverter },
{ provide: MapDrawingToDrawingConverter }, { provide: MapDrawingToDrawingConverter },
{ provide: MapLabelToLabelConverter },
{ provide: NodesDataSource }, { provide: NodesDataSource },
{ provide: LinksDataSource }, { provide: LinksDataSource },
{ provide: DrawingsDataSource, useValue: drawingsDataSource }, { provide: DrawingsDataSource, useValue: drawingsDataSource },
{ provide: SettingsService, useClass: MockedSettingsService }, { provide: SettingsService, useClass: MockedSettingsService },
{ provide: ToolsService } { provide: ToolsService },
{ provide: SelectionManager },
{ provide: SelectionTool }
], ],
declarations: [ProjectMapComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS], declarations: [ProjectMapComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@ -32,6 +32,14 @@ import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.compo
import { ToolsService } from '../../services/tools.service'; import { ToolsService } from '../../services/tools.service';
import { DrawingContextMenu } from '../../cartography/events/event-source'; import { DrawingContextMenu } from '../../cartography/events/event-source';
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter'; import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
import { SelectionManager } from '../../cartography/managers/selection-manager';
import { SelectionTool } from '../../cartography/tools/selection-tool';
import { MapDrawing } from '../../cartography/models/map/map-drawing';
import { MapLabel } from '../../cartography/models/map/map-label';
import { Label } from '../../cartography/models/label';
import { MapNode } from '../../cartography/models/map/map-node';
import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter';
@Component({ @Component({
selector: 'app-project-map', selector: 'app-project-map',
@ -86,11 +94,14 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private drawingsWidget: DrawingsWidget, private drawingsWidget: DrawingsWidget,
private mapNodeToNode: MapNodeToNodeConverter, private mapNodeToNode: MapNodeToNodeConverter,
private mapDrawingToDrawing: MapDrawingToDrawingConverter, private mapDrawingToDrawing: MapDrawingToDrawingConverter,
private mapLabelToLabel: MapLabelToLabelConverter,
private nodesDataSource: NodesDataSource, private nodesDataSource: NodesDataSource,
private linksDataSource: LinksDataSource, private linksDataSource: LinksDataSource,
private drawingsDataSource: DrawingsDataSource, private drawingsDataSource: DrawingsDataSource,
private settingsService: SettingsService, private settingsService: SettingsService,
private toolsService: ToolsService private toolsService: ToolsService,
private selectionManager: SelectionManager,
private selectionTool: SelectionTool
) {} ) {}
ngOnInit() { ngOnInit() {
@ -206,8 +217,30 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.clientY, eventDrawing.event.clientX); this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.clientY, eventDrawing.event.clientX);
}); });
const onContextMenu = this.selectionTool.contextMenuOpened.subscribe((event) => {
const selectedItems = this.selectionManager.getSelected();
if (selectedItems.length === 0) return;
let drawings: Drawing[] = [];
let nodes: Node[] = [];
let labels: Label[] = [];
selectedItems.forEach((elem) => {
if (elem instanceof MapDrawing) {
drawings.push(this.mapDrawingToDrawing.convert(elem));
} else if (elem instanceof MapNode) {
nodes.push(this.mapNodeToNode.convert(elem));
} else if (elem instanceof MapLabel) {
labels.push(this.mapLabelToLabel.convert(elem));
}
});
this.contextMenu.openMenuForListOfElements(drawings, nodes, labels, event.clientY, event.clientX);
});
this.subscriptions.push(onNodeContextMenu); this.subscriptions.push(onNodeContextMenu);
this.subscriptions.push(onDrawingContextMenu); this.subscriptions.push(onDrawingContextMenu);
this.subscriptions.push(onContextMenu);
this.mapChangeDetectorRef.detectChanges(); this.mapChangeDetectorRef.detectChanges();
} }

View File

@ -45,6 +45,10 @@ export class MockedProjectService {
add() { add() {
return of(this.projects.pop); return of(this.projects.pop);
} }
isReadOnly(project: Project){
return false;
}
} }
describe('AddBlankProjectDialogComponent', () => { describe('AddBlankProjectDialogComponent', () => {

View File

@ -34,7 +34,7 @@
})(); })();
</script> </script>
</head> </head>
<body class="mat-app-background"> <body class="mat-app-background" oncontextmenu="return false;">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>