mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-30 15:59:58 +00:00
Selection Manager
This commit is contained in:
parent
6dc7f870b2
commit
28ead48c40
@ -0,0 +1,57 @@
|
|||||||
|
import { Subject} from "rxjs/Subject";
|
||||||
|
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { Rectangle } from "../models/rectangle";
|
||||||
|
import { SelectionManager } from "./selection-manager";
|
||||||
|
import { NodesDataSource } from "../datasources/nodes-datasource";
|
||||||
|
import { LinksDataSource } from "../datasources/links-datasource";
|
||||||
|
import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper";
|
||||||
|
|
||||||
|
|
||||||
|
describe('SelectionManager', () => {
|
||||||
|
let manager: SelectionManager;
|
||||||
|
let selectedRectangleSubject: Subject<Rectangle>;
|
||||||
|
let nodesDataSource: NodesDataSource;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const linksDataSourec = new LinksDataSource();
|
||||||
|
const inRectangleHelper = new InRectangleHelper();
|
||||||
|
|
||||||
|
selectedRectangleSubject = new Subject<Rectangle>();
|
||||||
|
|
||||||
|
nodesDataSource = new NodesDataSource();
|
||||||
|
|
||||||
|
manager = new SelectionManager(nodesDataSource, linksDataSourec, inRectangleHelper);
|
||||||
|
manager.subscribe(selectedRectangleSubject);
|
||||||
|
|
||||||
|
const node_1 = new Node();
|
||||||
|
node_1.name = "Node 1";
|
||||||
|
node_1.x = 150;
|
||||||
|
node_1.y = 150;
|
||||||
|
|
||||||
|
nodesDataSource.add(node_1);
|
||||||
|
|
||||||
|
const node_2 = new Node();
|
||||||
|
node_2.name = "Node 2";
|
||||||
|
node_2.x = 300;
|
||||||
|
node_2.y = 300;
|
||||||
|
nodesDataSource.add(node_2);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('node should be selected', () => {
|
||||||
|
selectedRectangleSubject.next(new Rectangle(100, 100, 100, 100));
|
||||||
|
expect(nodesDataSource.getItems()[0].is_selected).toEqual(true);
|
||||||
|
expect(manager.getSelectedNodes().length).toEqual(1);
|
||||||
|
expect(manager.getSelectedLinks().length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('node should be selected and deselected', () => {
|
||||||
|
selectedRectangleSubject.next(new Rectangle(100, 100, 100, 100));
|
||||||
|
selectedRectangleSubject.next(new Rectangle(350, 350, 100, 100));
|
||||||
|
expect(nodesDataSource.getItems()[0].is_selected).toEqual(false);
|
||||||
|
expect(manager.getSelectedNodes().length).toEqual(0);
|
||||||
|
expect(manager.getSelectedLinks().length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,10 +1,16 @@
|
|||||||
import {Node} from "../models/node";
|
import { Injectable } from "@angular/core";
|
||||||
import {NodesDataSource} from "../datasources/nodes-datasource";
|
|
||||||
import {LinksDataSource} from "../datasources/links-datasource";
|
import { Subject } from "rxjs/Subject";
|
||||||
import {Injectable} from "@angular/core";
|
import { Subscription } from "rxjs/Subscription";
|
||||||
import {InRectangleHelper} from "../../map/helpers/in-rectangle-helper";
|
|
||||||
import {Rectangle} from "../models/rectangle";
|
import { NodesDataSource } from "../datasources/nodes-datasource";
|
||||||
import {Link} from "../models/link";
|
import { LinksDataSource } from "../datasources/links-datasource";
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper";
|
||||||
|
import { Rectangle } from "../models/rectangle";
|
||||||
|
import { Link } from "../models/link";
|
||||||
|
import { DataSource } from "../datasources/datasource";
|
||||||
|
|
||||||
|
|
||||||
export interface Selectable {
|
export interface Selectable {
|
||||||
x: number;
|
x: number;
|
||||||
@ -12,40 +18,44 @@ export interface Selectable {
|
|||||||
is_selected: boolean;
|
is_selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SelectionManager {
|
export class SelectionManager {
|
||||||
private selectedItems: Selectable[] = [];
|
private selectedNodes: Node[] = [];
|
||||||
|
private selectedLinks: Link[] = [];
|
||||||
|
private subscription: Subscription;
|
||||||
|
|
||||||
constructor(private nodesDataSource: NodesDataSource,
|
constructor(private nodesDataSource: NodesDataSource,
|
||||||
private linksDataSource: LinksDataSource,
|
private linksDataSource: LinksDataSource,
|
||||||
private inRectangleHelper: InRectangleHelper) {}
|
private inRectangleHelper: InRectangleHelper) {}
|
||||||
|
|
||||||
public setSelectedItemsInRectangle(rectangle: Rectangle) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const nodes: Node[] = [];
|
public subscribe(subject: Subject<Rectangle>) {
|
||||||
this.nodesDataSource.getItems().forEach((node: Node) => {
|
this.subscription = subject.subscribe((rectangle: Rectangle) => {
|
||||||
const is_selected = self.inRectangleHelper.inRectangle(node, rectangle);
|
this.selectedNodes = this.getSelectedItemsInRectangle<Node>(this.nodesDataSource, rectangle);
|
||||||
if (node.is_selected !== is_selected) {
|
this.selectedLinks = this.getSelectedItemsInRectangle<Link>(this.linksDataSource, rectangle);
|
||||||
node.is_selected = is_selected;
|
});
|
||||||
self.nodesDataSource.update(node);
|
}
|
||||||
|
|
||||||
|
public getSelectedNodes() {
|
||||||
|
return this.selectedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSelectedLinks() {
|
||||||
|
return this.selectedLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectedItemsInRectangle<T extends Selectable>(dataSource: DataSource<T>, rectangle: Rectangle) {
|
||||||
|
const items: T[] = [];
|
||||||
|
dataSource.getItems().forEach((item: T) => {
|
||||||
|
const is_selected = this.inRectangleHelper.inRectangle(item, rectangle);
|
||||||
|
if (item.is_selected !== is_selected) {
|
||||||
|
item.is_selected = is_selected;
|
||||||
|
dataSource.update(item);
|
||||||
if (is_selected) {
|
if (is_selected) {
|
||||||
nodes.push(node);
|
items.push(item);
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const links: Link[] = [];
|
|
||||||
this.linksDataSource.getItems().forEach((link: Link) => {
|
|
||||||
const is_selected = self.inRectangleHelper.inRectangle(link, rectangle);
|
|
||||||
if (link.is_selected !== is_selected) {
|
|
||||||
link.is_selected = is_selected;
|
|
||||||
self.linksDataSource.update(link);
|
|
||||||
if (is_selected) {
|
|
||||||
links.push(link);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import {SelectionTool} from "./selection-tool";
|
import { select } from "d3-selection";
|
||||||
import {select} from "d3-selection";
|
|
||||||
import {Context} from "../../../map/models/context";
|
|
||||||
import {SVGSelection} from "../../../map/models/types";
|
|
||||||
import {Node} from "../models/node";
|
|
||||||
|
|
||||||
|
import { SelectionTool } from "./selection-tool";
|
||||||
|
import { Context } from "../../../map/models/context";
|
||||||
|
import { SVGSelection } from "../../../map/models/types";
|
||||||
|
import { Rectangle } from "../models/rectangle";
|
||||||
|
|
||||||
class OnSelectedListenerMock {
|
|
||||||
public constructor(public nodes: Node[] = []) {}
|
|
||||||
|
|
||||||
public listen(nodes: Node[]) {
|
|
||||||
this.nodes = nodes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SelectionTool', () => {
|
describe('SelectionTool', () => {
|
||||||
let tool: SelectionTool;
|
let tool: SelectionTool;
|
||||||
@ -19,13 +12,13 @@ describe('SelectionTool', () => {
|
|||||||
let context: Context;
|
let context: Context;
|
||||||
let selection_line_tool: SVGSelection;
|
let selection_line_tool: SVGSelection;
|
||||||
let path_selection: SVGSelection;
|
let path_selection: SVGSelection;
|
||||||
let selected_nodes: Node[];
|
let selected_rectangle: Rectangle;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tool = new SelectionTool();
|
tool = new SelectionTool();
|
||||||
|
|
||||||
tool.selectedSubject.subscribe((nodes: Node[]) => {
|
tool.rectangleSelected.subscribe((rectangle: Rectangle) => {
|
||||||
selected_nodes = nodes;
|
selected_rectangle = rectangle;
|
||||||
});
|
});
|
||||||
|
|
||||||
svg = select('body')
|
svg = select('body')
|
||||||
@ -33,25 +26,6 @@ describe('SelectionTool', () => {
|
|||||||
.attr('width', 1000)
|
.attr('width', 1000)
|
||||||
.attr('height', 1000);
|
.attr('height', 1000);
|
||||||
|
|
||||||
const node_1 = new Node();
|
|
||||||
node_1.name = "Node 1";
|
|
||||||
node_1.x = 150;
|
|
||||||
node_1.y = 150;
|
|
||||||
|
|
||||||
const node_2 = new Node();
|
|
||||||
node_2.name = "Node 2";
|
|
||||||
node_2.x = 300;
|
|
||||||
node_2.y = 300;
|
|
||||||
|
|
||||||
svg.selectAll<SVGGElement, any>('g.node')
|
|
||||||
.data([node_1, node_2], (n: Node) => {
|
|
||||||
return n.node_id;
|
|
||||||
})
|
|
||||||
.enter()
|
|
||||||
.append<SVGGElement>('g')
|
|
||||||
.attr('class', 'node selectable');
|
|
||||||
|
|
||||||
|
|
||||||
svg.append<SVGGElement>('g').attr('class', 'canvas');
|
svg.append<SVGGElement>('g').attr('class', 'canvas');
|
||||||
|
|
||||||
context = new Context();
|
context = new Context();
|
||||||
@ -103,13 +77,8 @@ describe('SelectionTool', () => {
|
|||||||
expect(path_selection.attr('visibility')).toEqual('hidden');
|
expect(path_selection.attr('visibility')).toEqual('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('node should be selected', () => {
|
it('rectangle should be selected', () => {
|
||||||
expect(svg.selectAll('.selected').size()).toEqual(1);
|
expect(selected_rectangle).toEqual(new Rectangle(95, 86, 100, 100));
|
||||||
expect(svg.select('.selected').datum().name).toEqual("Node 1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selectedSubject should update nodes', () => {
|
|
||||||
expect(selected_nodes.length).toEqual(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SelectionTool can deselect after click outside', () => {
|
describe('SelectionTool can deselect after click outside', () => {
|
||||||
@ -118,15 +87,10 @@ describe('SelectionTool', () => {
|
|||||||
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300}));
|
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have no selection', () => {
|
it('rectangle should be selected', () => {
|
||||||
expect(svg.selectAll('.selected').size()).toEqual(0);
|
expect(selected_rectangle).toEqual(new Rectangle(295, 286, 0, 0));
|
||||||
});
|
|
||||||
|
|
||||||
it('selectedSubject should clear nodes', () => {
|
|
||||||
expect(selected_nodes.length).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SelectionTool can handle end of selection in reverse direction', () => {
|
describe('SelectionTool can handle end of selection in reverse direction', () => {
|
||||||
@ -136,8 +100,8 @@ describe('SelectionTool', () => {
|
|||||||
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 100, clientY: 100}));
|
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 100, clientY: 100}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('node should be selected', () => {
|
it('rectangle should be selected', () => {
|
||||||
expect(svg.select('.selected').datum().name).toEqual("Node 1");
|
expect(selected_rectangle).toEqual(new Rectangle(95, 86, 100, 100));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,20 +3,21 @@ import {mouse, select} from "d3-selection";
|
|||||||
import {Context} from "../../../map/models/context";
|
import {Context} from "../../../map/models/context";
|
||||||
import {Subject} from "rxjs/Subject";
|
import {Subject} from "rxjs/Subject";
|
||||||
import {Rectangle} from "../models/rectangle";
|
import {Rectangle} from "../models/rectangle";
|
||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export class SelectionTool {
|
export class SelectionTool {
|
||||||
static readonly SELECTABLE_CLASS = '.selectable';
|
static readonly SELECTABLE_CLASS = '.selectable';
|
||||||
|
|
||||||
public rectangleSelected = new Subject<Rectangle>();
|
public rectangleSelected: Subject<Rectangle>;
|
||||||
|
|
||||||
private selection: SVGSelection;
|
private selection: SVGSelection;
|
||||||
private path;
|
private path;
|
||||||
private context: Context;
|
private context: Context;
|
||||||
// public selectedSubject: Subject<Selectable[]>;
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
// this.selectedSubject = new Subject<Selectable[]>();
|
this.rectangleSelected = new Subject<Rectangle>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(selection: SVGSelection, context: Context) {
|
public connect(selection: SVGSelection, context: Context) {
|
||||||
@ -87,8 +88,7 @@ export class SelectionTool {
|
|||||||
|
|
||||||
private endSelection(start, end) {
|
private endSelection(start, end) {
|
||||||
this.path.attr("visibility", "hidden");
|
this.path.attr("visibility", "hidden");
|
||||||
// const selected_items = this.getSelectedItems(start, end);
|
this.selectedEvent(start, end);
|
||||||
// this.selectedSubject.next(selected_items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectedEvent(start, end) {
|
private selectedEvent(start, end) {
|
||||||
@ -99,27 +99,6 @@ export class SelectionTool {
|
|||||||
this.rectangleSelected.next(new Rectangle(x, y, width, height));
|
this.rectangleSelected.next(new Rectangle(x, y, width, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
// private getSelectedItems(start, end): Selectable[] {
|
|
||||||
// const x = Math.min(start[0], end[0]);
|
|
||||||
// const y = Math.min(start[1], end[1]);
|
|
||||||
// const width = Math.abs(start[0] - end[0]);
|
|
||||||
// const height = Math.abs(start[1] - end[1]);
|
|
||||||
// const items: Selectable[] = [];
|
|
||||||
//
|
|
||||||
// this.selection
|
|
||||||
// .selectAll<null, Selectable>(SelectionTool.SELECTABLE_CLASS)
|
|
||||||
// .classed('selected', (item: Selectable) => {
|
|
||||||
//
|
|
||||||
// const in_rect = (x <= item.x && item.x < (x + width) && y <= item.y && item.y < (y + height));
|
|
||||||
// if (in_rect) {
|
|
||||||
// items.push(item);
|
|
||||||
// }
|
|
||||||
// return in_rect;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// return items;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private rect(x: number, y: number, w: number, h: number) {
|
private rect(x: number, y: number, w: number, h: number) {
|
||||||
return "M" + [x, y] + " l" + [w, 0] + " l" + [0, h] + " l" + [-w, 0] + "z";
|
return "M" + [x, y] + " l" + [w, 0] + " l" + [0, h] + " l" + [-w, 0] + "z";
|
||||||
}
|
}
|
||||||
|
@ -182,9 +182,7 @@ export class ProjectMapComponent implements OnInit {
|
|||||||
|
|
||||||
const selectionManager = new SelectionManager(this.nodesDataSource, this.linksDataSource, new InRectangleHelper());
|
const selectionManager = new SelectionManager(this.nodesDataSource, this.linksDataSource, new InRectangleHelper());
|
||||||
|
|
||||||
this.mapChild.graphLayout.getSelectionTool().rectangleSelected.subscribe((rectangle: Rectangle) => {
|
selectionManager.subscribe(this.mapChild.graphLayout.getSelectionTool().rectangleSelected);
|
||||||
selectionManager.setSelectedItemsInRectangle(rectangle);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNodeCreation(appliance: Appliance) {
|
onNodeCreation(appliance: Appliance) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user