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 { SelectionManager } from '../../managers/selection-manager';
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
@ -12,7 +12,10 @@ export class SelectionSelectComponent implements OnInit, OnDestroy {
private onSelected: Subscription;
private onUnselected: Subscription;
constructor(private selectionManager: SelectionManager, private mapChangeDetectorRef: MapChangeDetectorRef) {}
constructor(
private selectionManager: SelectionManager,
private mapChangeDetectorRef: MapChangeDetectorRef
) {}
ngOnInit() {
this.onSelected = this.selectionManager.selected.subscribe(() => {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { mouse, select } from 'd3-selection';
import { Injectable, EventEmitter } from '@angular/core';
import { mouse, select, event } from 'd3-selection';
import { Subject } from 'rxjs';
import { SVGSelection } from '../models/types';
@ -12,20 +12,39 @@ export class SelectionTool {
static readonly SELECTABLE_CLASS = '.selectable';
public rectangleSelected = new Subject<Rectangle>();
public contextMenuOpened = new EventEmitter<any>();
private path;
private enabled = false;
public constructor(private context: Context, private selectionEventSource: SelectionEventSource) {}
public constructor(
private context: Context,
private selectionEventSource: SelectionEventSource
) {}
public disableContextMenu(){
}
public setEnabled(enabled) {
this.enabled = enabled;
this.contextMenuOpened.emit(true);
}
private activate(selection) {
const self = this;
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 parent = this.parentElement;

View File

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

View File

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

View File

@ -9,13 +9,22 @@ import { Node } from '../../../../../cartography/models/node';
})
export class StopNodeActionComponent implements OnInit {
@Input() server: Server;
@Input() node: Node;
@Input() nodes: Node[];
private isNodeWithStartedStatus: boolean;
constructor(private nodeService: NodeService) {}
ngOnInit() {}
ngOnInit() {
this.nodes.forEach((node) => {
if (node.status === 'started') {
this.isNodeWithStartedStatus = true;
}
});
}
stopNode() {
this.nodeService.stop(this.server, this.node).subscribe((n: Node) => {});
stopNodes() {
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>
<mat-menu #contextMenu="matMenu" class="context-menu-items">
<app-start-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-start-node-action>
<app-stop-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-stop-node-action>
<app-edit-style-action
*ngIf="hasDrawingCapabilities && !isTextElement"
<app-start-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-start-node-action>
<app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
<app-edit-style-action *ngIf="drawings.length===1 && !hasTextCapabilities"
[server]="server"
[project]="project"
[drawing]="drawing"
[drawing]="drawings[0]"
></app-edit-style-action>
<app-edit-text-action
*ngIf="hasDrawingCapabilities && isTextElement"
*ngIf="drawings.length===1 && hasTextCapabilities"
[server]="server"
[project]="project"
[drawing]="drawing"
[drawing]="drawings[0]"
></app-edit-text-action>
<app-move-layer-up-action
*ngIf="!projectService.isReadOnly(project)"
[server]="server"
[node]="node"
[drawing]="drawing"
[nodes]="nodes"
[drawings]="drawings"
></app-move-layer-up-action>
<app-move-layer-down-action
*ngIf="!projectService.isReadOnly(project)"
[server]="server"
[node]="node"
[drawing]="drawing"
[nodes]="nodes"
[drawings]="drawings"
></app-move-layer-down-action>
</mat-menu>
</div>

View File

@ -16,7 +16,10 @@ describe('ContextMenuComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatMenuModule, BrowserModule],
providers: [{ provide: ChangeDetectorRef }, { provide: ProjectService, useClass: MockedProjectService }],
providers: [
{ provide: ChangeDetectorRef },
{ provide: ProjectService, useClass: MockedProjectService }
],
declarations: [ContextMenuComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@ -62,4 +65,13 @@ describe('ContextMenuComponent', () => {
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 { Drawing } from '../../../cartography/models/drawing';
import { TextElement } from '../../../cartography/models/drawings/text-element';
import { Label } from '../../../cartography/models/label';
@Component({
selector: 'app-context-menu',
@ -21,11 +23,12 @@ export class ContextMenuComponent implements OnInit {
topPosition;
leftPosition;
node: Node;
drawing: Drawing;
private hasNodeCapabilities: boolean = false;
private hasDrawingCapabilities: boolean = false;
private isTextElement: boolean = false;
drawings: Drawing[] = [];
nodes: Node[] = [];
labels: Label[] = [];
private hasTextCapabilities: boolean = false;
constructor(
private sanitizer: DomSanitizer,
@ -43,32 +46,49 @@ export class ContextMenuComponent implements OnInit {
this.changeDetector.detectChanges();
}
public openMenuForNode(node: Node, top: number, left: number) {
public openMenuForDrawing(drawing: Drawing, top: number, left: number) {
this.resetCapabilities();
this.hasNodeCapabilities = true;
this.hasTextCapabilities = drawing.element instanceof TextElement;
this.node = node;
this.drawings = [drawing];
this.setPosition(top, left);
this.contextMenu.openMenu();
}
public openMenuForDrawing(drawing: Drawing, top: number, left: number) {
public openMenuForNode(node: Node, top: number, left: number) {
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.contextMenu.openMenu();
}
private resetCapabilities() {
this.node = null;
this.drawing = null;
this.hasDrawingCapabilities = false;
this.hasNodeCapabilities = false;
this.isTextElement = false;
this.drawings = [];
this.nodes = [];
this.labels = [];
this.hasTextCapabilities = false;
}
}

View File

@ -33,6 +33,9 @@ import { Node } from '../../cartography/models/node';
import { ToolsService } from '../../services/tools.service';
import { DrawingsWidget } from '../../cartography/widgets/drawings';
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 {
public activate() {}
@ -139,11 +142,14 @@ describe('ProjectMapComponent', () => {
{ provide: DrawingsWidget },
{ provide: MapNodeToNodeConverter },
{ provide: MapDrawingToDrawingConverter },
{ provide: MapLabelToLabelConverter },
{ provide: NodesDataSource },
{ provide: LinksDataSource },
{ provide: DrawingsDataSource, useValue: drawingsDataSource },
{ provide: SettingsService, useClass: MockedSettingsService },
{ provide: ToolsService }
{ provide: ToolsService },
{ provide: SelectionManager },
{ provide: SelectionTool }
],
declarations: [ProjectMapComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
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 { DrawingContextMenu } from '../../cartography/events/event-source';
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({
selector: 'app-project-map',
@ -86,11 +94,14 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private drawingsWidget: DrawingsWidget,
private mapNodeToNode: MapNodeToNodeConverter,
private mapDrawingToDrawing: MapDrawingToDrawingConverter,
private mapLabelToLabel: MapLabelToLabelConverter,
private nodesDataSource: NodesDataSource,
private linksDataSource: LinksDataSource,
private drawingsDataSource: DrawingsDataSource,
private settingsService: SettingsService,
private toolsService: ToolsService
private toolsService: ToolsService,
private selectionManager: SelectionManager,
private selectionTool: SelectionTool
) {}
ngOnInit() {
@ -206,8 +217,30 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
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(onDrawingContextMenu);
this.subscriptions.push(onContextMenu);
this.mapChangeDetectorRef.detectChanges();
}

View File

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

View File

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