mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-02-22 18:22:35 +00:00
Initial implementation of text editing
This commit is contained in:
parent
acac9edf0e
commit
c0bfc3ed35
@ -5,6 +5,7 @@ import { MapDrawing } from '../../models/map/map-drawing';
|
||||
import { RectElement } from '../../models/drawings/rect-element';
|
||||
import { EllipseElement } from '../../models/drawings/ellipse-element';
|
||||
import { LineElement } from '../../models/drawings/line-element';
|
||||
import { TextElement } from '../../models/drawings/text-element';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -21,6 +22,8 @@ export class MapDrawingToSvgConverter implements Converter<MapDrawing, string> {
|
||||
elem = `<ellipse fill=\"${mapDrawing.element.fill}\" fill-opacity=\"${mapDrawing.element.fill_opacity}\" cx=\"${mapDrawing.element.cx}\" cy=\"${mapDrawing.element.cy}\" rx=\"${mapDrawing.element.rx}\" ry=\"${mapDrawing.element.ry}\" stroke=\"${mapDrawing.element.stroke}\" stroke-width=\"${mapDrawing.element.stroke_width}\" />`;
|
||||
} else if (mapDrawing.element instanceof LineElement) {
|
||||
elem = `<line stroke=\"${mapDrawing.element.stroke}\" stroke-width=\"${mapDrawing.element.stroke_width}\" x1=\"${mapDrawing.element.x1}\" x2=\"${mapDrawing.element.x2}\" y1=\"${mapDrawing.element.y1}\" y2=\"${mapDrawing.element.y2}\" />`
|
||||
} else if (mapDrawing.element instanceof TextElement) {
|
||||
elem = `<text fill=\"${mapDrawing.element.fill}\" fill-opacity=\"${mapDrawing.element.fill_opacity}\" font-family=\"${mapDrawing.element.font_family}\" font-size=\"${mapDrawing.element.font_size}\" font-weight=\"${mapDrawing.element.font_weight}\">${mapDrawing.element.text}</text>`;
|
||||
} else return "";
|
||||
|
||||
return `<svg height=\"${mapDrawing.element.height}\" width=\"${mapDrawing.element.width}\">${elem}</svg>`;
|
||||
|
@ -14,8 +14,8 @@ export class ResizedDataEvent<T> {
|
||||
public x: number,
|
||||
public y: number,
|
||||
public width: number,
|
||||
public height: number) {
|
||||
}
|
||||
public height: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ClickedDataEvent<T> {
|
||||
@ -25,3 +25,10 @@ export class ClickedDataEvent<T> {
|
||||
public y: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export class EditedDataEvent {
|
||||
constructor(
|
||||
public id: string,
|
||||
public editedText: string
|
||||
) {}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ export class SelectionManager {
|
||||
this.selection = dictItems;
|
||||
|
||||
if (selected.length > 0) {
|
||||
//console.log(selected);
|
||||
|
||||
this.selected.emit(selected);
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,8 @@ export class SelectionTool {
|
||||
private activate(selection) {
|
||||
const self = this;
|
||||
|
||||
console.log("test!!!", selection);
|
||||
|
||||
selection.on("mousedown", function() {
|
||||
const subject = select(window);
|
||||
const parent = this.parentElement;
|
||||
|
18
src/app/cartography/tools/text-editing-tool.ts
Normal file
18
src/app/cartography/tools/text-editing-tool.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Injectable, EventEmitter } from "@angular/core";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class TextEditingTool {
|
||||
static readonly EDITING_CLASS = '.text-editing';
|
||||
|
||||
private enabled = true;
|
||||
public editingFinished = new EventEmitter<any>();
|
||||
|
||||
public setEnabled(enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public activate(){
|
||||
|
||||
}
|
||||
}
|
@ -24,10 +24,91 @@ export class TextDrawingWidget implements DrawingShapeWidget {
|
||||
return (d.element && d.element instanceof TextElement) ? [d.element] : [];
|
||||
});
|
||||
|
||||
// drawing.enter()
|
||||
// .append("foreignObject")
|
||||
// .attr("width", (elem) => elem.width)
|
||||
// .attr("height", (elem) => elem.height)
|
||||
// .attr("visibility", "hidden")
|
||||
// .append("xhtml:div")
|
||||
// .attr("width", (elem) => elem.width)
|
||||
// .attr("height", (elem) => elem.height)
|
||||
// .attr('style', (text: TextElement) => {
|
||||
// const font = this.fontFixer.fix(text);
|
||||
|
||||
// const styles: string[] = [];
|
||||
// if (font.font_family) {
|
||||
// styles.push(`font-family: "${text.font_family}"`);
|
||||
// }
|
||||
// if (font.font_size) {
|
||||
// styles.push(`font-size: ${text.font_size}pt`);
|
||||
// }
|
||||
// if (font.font_weight) {
|
||||
// styles.push(`font-weight: ${text.font_weight}`);
|
||||
// }
|
||||
// styles.push(`color: ${text.fill}`);
|
||||
// return styles.join("; ");
|
||||
// })
|
||||
// .attr('text-decoration', (text) => text.text_decoration)
|
||||
// .attr('contenteditable', 'true')
|
||||
// .text((elem) => elem.text)
|
||||
// .on("dblclick", (_, index, textElements) => {
|
||||
// select(textElements[index]).attr("visibility", "visible");
|
||||
// });
|
||||
|
||||
const drawing_enter = drawing
|
||||
.enter()
|
||||
.append<SVGTextElement>('text')
|
||||
.attr('class', 'text_element noselect');
|
||||
.attr('class', 'text_element noselect')
|
||||
.on("dblclick", (elem, index, textElements) => {
|
||||
console.log("Id: ", textElements[index].parentElement.parentElement.getAttribute("drawing_id"));
|
||||
|
||||
select(textElements[index])
|
||||
.attr("visibility", "hidden");
|
||||
|
||||
select(textElements[index])
|
||||
.classed("editingMode", true);
|
||||
|
||||
select(textElements[index].parentElement.parentElement.parentElement)
|
||||
.append("foreignObject")
|
||||
.attr("width", '1000px')
|
||||
.attr("min-width", 'fit-content')
|
||||
.attr("height", '100px')
|
||||
.attr("id", "temporaryText")
|
||||
.attr("transform", textElements[index].parentElement.getAttribute("transform"))
|
||||
.append("xhtml:div")
|
||||
.attr("width", "fit-content")
|
||||
.attr("height", "fit-content")
|
||||
.attr("class", "temporaryTextInside")
|
||||
.attr('style', () => {
|
||||
const styles: string[] = [];
|
||||
styles.push(`white-space: pre-line`)
|
||||
styles.push(`outline: 0px solid transparent`)
|
||||
styles.push(`font-family: ${elem.font_family}`)
|
||||
styles.push(`font-size: ${elem.font_size}pt!important`);
|
||||
styles.push(`font-weight: ${elem.font_weight}`)
|
||||
styles.push(`color: ${elem.fill}`);
|
||||
return styles.join("; ");
|
||||
})
|
||||
.attr('text-decoration', elem.text_decoration)
|
||||
.attr('contenteditable', 'true')
|
||||
.text(elem.text)
|
||||
.on("focusout", (elem, index, textElements) => {
|
||||
let temporaryText = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement;
|
||||
let savedText = temporaryText.innerText;
|
||||
|
||||
//let splittedText = savedText.split(/\r?\n/);
|
||||
var temporaryElement = document.getElementById("temporaryText") as HTMLElement;
|
||||
temporaryElement.remove();
|
||||
|
||||
view.selectAll<SVGTextElement, TextElement>('text.editingMode')
|
||||
.attr("visibility", "visible")
|
||||
.classed("editingMode", false)
|
||||
.text(savedText);
|
||||
});
|
||||
|
||||
var txtInside = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement;
|
||||
txtInside.focus();
|
||||
});
|
||||
|
||||
const merge = drawing.merge(drawing_enter);
|
||||
merge
|
||||
@ -75,6 +156,7 @@ export class TextDrawingWidget implements DrawingShapeWidget {
|
||||
// approx and make it matching to GUI
|
||||
const tspan = select(this).selectAll<SVGTSpanElement, string>('tspan');
|
||||
const height = this.getBBox().height / tspan.size();
|
||||
//return `translate(0, ${height})`;
|
||||
return `translate(${TextDrawingWidget.MARGIN}, ${height - TextDrawingWidget.MARGIN})`;
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,7 @@ g.node:hover {
|
||||
background: transparent;
|
||||
top: 20px;
|
||||
left: 92px;
|
||||
width: 232px !important;
|
||||
width: 296px !important;
|
||||
height: 72px !important;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ g.node:hover {
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 232px !important;
|
||||
width: 296px !important;
|
||||
height: 72px !important;
|
||||
background:#263238;
|
||||
}
|
||||
@ -51,7 +51,6 @@ g.node:hover {
|
||||
}
|
||||
|
||||
.drawer-button {
|
||||
height: 72px;
|
||||
width: 64px!important;
|
||||
background: #263238;
|
||||
padding: 0;
|
||||
@ -70,8 +69,7 @@ g.node:hover {
|
||||
|
||||
.drawer-arrow-button-left {
|
||||
width: 40px;
|
||||
height: 72px;
|
||||
margin-left: 192px;
|
||||
margin-left: 256px;
|
||||
background:#263238;
|
||||
position: fixed;
|
||||
}
|
||||
|
@ -102,6 +102,9 @@
|
||||
<mat-drawer-container [ngClass]="{shadow: drawTools.visibility}" class="drawer-container">
|
||||
<mat-drawer #drawer class="drawer">
|
||||
<div class="drawer-buttons">
|
||||
<button matTooltip="Add a note" mat-icon-button class="drawer-button" [color]="drawTools.isAddingTextChosen ? 'primary': 'basic'" (click)="addText()">
|
||||
<mat-icon>create</mat-icon>
|
||||
</button>
|
||||
<button matTooltip="Draw a rectangle" mat-icon-button class="drawer-button" [color]="drawTools.isRectangleChosen ? 'primary': 'basic'" (click)="addDrawing('rectangle')">
|
||||
<mat-icon>crop_3_2</mat-icon>
|
||||
</button>
|
||||
|
@ -79,7 +79,7 @@ export class MockedDrawingsDataSource {
|
||||
update() { return of({})}
|
||||
}
|
||||
|
||||
fdescribe('ProjectMapComponent', () => {
|
||||
describe('ProjectMapComponent', () => {
|
||||
let component: ProjectMapComponent;
|
||||
let fixture: ComponentFixture<ProjectMapComponent>;
|
||||
let drawingService = new MockedDrawingService;
|
||||
|
@ -27,7 +27,7 @@ import { MapChangeDetectorRef } from '../../cartography/services/map-change-dete
|
||||
import { NodeContextMenu } from '../../cartography/events/nodes';
|
||||
import { MapLinkCreated } from '../../cartography/events/links';
|
||||
import { NodeWidget } from '../../cartography/widgets/node';
|
||||
import { DraggedDataEvent, ResizedDataEvent } from '../../cartography/events/event-source';
|
||||
import { DraggedDataEvent, ResizedDataEvent, EditedDataEvent } from '../../cartography/events/event-source';
|
||||
import { DrawingService } from '../../services/drawing.service';
|
||||
import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-node-to-node-converter';
|
||||
import { NodesEventSource } from '../../cartography/events/nodes-event-source';
|
||||
@ -45,6 +45,9 @@ import { SettingsService, Settings } from '../../services/settings.service';
|
||||
import { MapLabel } from '../../cartography/models/map/map-label';
|
||||
import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component';
|
||||
import { MapLinkNode } from '../../cartography/models/map/map-link-node';
|
||||
import { TextElement } from '../../cartography/models/drawings/text-element';
|
||||
import { select } from 'd3-selection';
|
||||
import { FontFixer } from '../../cartography/helpers/font-fixer';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -75,7 +78,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
'isRectangleChosen': false,
|
||||
'isEllipseChosen': false,
|
||||
'isLineChosen': false,
|
||||
'visibility': false
|
||||
'visibility': false,
|
||||
'isAddingTextChosen': false
|
||||
};
|
||||
|
||||
protected selectedDrawing: string;
|
||||
@ -344,6 +348,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public onDrawingEdited(editedEvent: EditedDataEvent){
|
||||
|
||||
}
|
||||
|
||||
public set readonly(value) {
|
||||
this.inReadOnlyMode = value;
|
||||
if (value) {
|
||||
@ -382,16 +390,19 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
|
||||
switch (selectedObject) {
|
||||
case "rectangle":
|
||||
this.drawTools.isAddingTextChosen = false;
|
||||
this.drawTools.isEllipseChosen = false;
|
||||
this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen;
|
||||
this.drawTools.isLineChosen = false;
|
||||
break;
|
||||
case "ellipse":
|
||||
this.drawTools.isAddingTextChosen = false;
|
||||
this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen;
|
||||
this.drawTools.isRectangleChosen = false;
|
||||
this.drawTools.isLineChosen = false;
|
||||
break;
|
||||
case "line":
|
||||
this.drawTools.isAddingTextChosen = false;
|
||||
this.drawTools.isEllipseChosen = false;
|
||||
this.drawTools.isRectangleChosen = false;
|
||||
this.drawTools.isLineChosen = !this.drawTools.isLineChosen;
|
||||
@ -424,6 +435,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.drawTools.isRectangleChosen = false;
|
||||
this.drawTools.isEllipseChosen = false;
|
||||
this.drawTools.isLineChosen = false;
|
||||
this.drawTools.isAddingTextChosen = false;
|
||||
this.selectedDrawing = "";
|
||||
}
|
||||
|
||||
@ -438,45 +450,58 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.drawTools.visibility = true;
|
||||
}
|
||||
|
||||
public getDrawingMock(objectType: string): MapDrawing {
|
||||
public getDrawingMock(objectType: string, text?: string): MapDrawing {
|
||||
let drawingElement: DrawingElement;
|
||||
|
||||
switch (objectType) {
|
||||
case "rectangle":
|
||||
let rect = new RectElement();
|
||||
rect.fill = "#ffffff";
|
||||
rect.fill_opacity = 1.0;
|
||||
rect.stroke = "#000000";
|
||||
rect.stroke_width = 2;
|
||||
rect.width = 200;
|
||||
rect.height = 100;
|
||||
drawingElement = rect;
|
||||
let rectElement = new RectElement();
|
||||
rectElement.fill = "#ffffff";
|
||||
rectElement.fill_opacity = 1.0;
|
||||
rectElement.stroke = "#000000";
|
||||
rectElement.stroke_width = 2;
|
||||
rectElement.width = 200;
|
||||
rectElement.height = 100;
|
||||
drawingElement = rectElement;
|
||||
break;
|
||||
case "ellipse":
|
||||
let ellipse = new EllipseElement();
|
||||
ellipse.fill = "#ffffff";
|
||||
ellipse.fill_opacity = 1.0;
|
||||
ellipse.stroke = "#000000";
|
||||
ellipse.stroke_width = 2;
|
||||
ellipse.cx = 100;
|
||||
ellipse.cy = 100;
|
||||
ellipse.rx = 100;
|
||||
ellipse.ry = 100;
|
||||
ellipse.width = 200;
|
||||
ellipse.height = 200;
|
||||
drawingElement = ellipse;
|
||||
let ellipseElement = new EllipseElement();
|
||||
ellipseElement.fill = "#ffffff";
|
||||
ellipseElement.fill_opacity = 1.0;
|
||||
ellipseElement.stroke = "#000000";
|
||||
ellipseElement.stroke_width = 2;
|
||||
ellipseElement.cx = 100;
|
||||
ellipseElement.cy = 100;
|
||||
ellipseElement.rx = 100;
|
||||
ellipseElement.ry = 100;
|
||||
ellipseElement.width = 200;
|
||||
ellipseElement.height = 200;
|
||||
drawingElement = ellipseElement;
|
||||
break;
|
||||
case "line":
|
||||
let line = new LineElement();
|
||||
line.stroke = "#000000";
|
||||
line.stroke_width = 2;
|
||||
line.x1 = 0;
|
||||
line.x2 = 200;
|
||||
line.y1 = 0;
|
||||
line.y2 = 0;
|
||||
line.width = 100;
|
||||
line.height = 0;
|
||||
drawingElement = line;
|
||||
let lineElement = new LineElement();
|
||||
lineElement.stroke = "#000000";
|
||||
lineElement.stroke_width = 2;
|
||||
lineElement.x1 = 0;
|
||||
lineElement.x2 = 200;
|
||||
lineElement.y1 = 0;
|
||||
lineElement.y2 = 0;
|
||||
lineElement.width = 100;
|
||||
lineElement.height = 0;
|
||||
drawingElement = lineElement;
|
||||
break;
|
||||
case "text":
|
||||
let textElement = new TextElement();
|
||||
textElement.height = 100; //should be calculated
|
||||
textElement.width = 100;
|
||||
textElement.text = text;
|
||||
textElement.fill = "#000000";
|
||||
textElement.fill_opacity = 0;
|
||||
textElement.font_family = "Noto Sans";
|
||||
textElement.font_size = 11;
|
||||
textElement.font_weight = "bold";
|
||||
drawingElement = textElement;
|
||||
break;
|
||||
}
|
||||
|
||||
let mapDrawing = new MapDrawing();
|
||||
@ -484,6 +509,68 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
return mapDrawing;
|
||||
}
|
||||
|
||||
public addText(){
|
||||
if (!this.drawTools.isAddingTextChosen){
|
||||
this.resetDrawToolChoice();
|
||||
this.drawTools.isAddingTextChosen = true;
|
||||
var map = document.getElementsByClassName('map')[0];
|
||||
|
||||
let addTextListener = (event: MouseEvent) => {
|
||||
let x = event.clientX - this.mapChild.context.getZeroZeroTransformationPoint().x;
|
||||
let y = event.clientY - this.mapChild.context.getZeroZeroTransformationPoint().y;
|
||||
|
||||
const svgElement = select("g.canvas");
|
||||
svgElement
|
||||
.append("foreignObject")
|
||||
.attr("id", "temporaryText")
|
||||
.attr("transform", `translate(${x},${y}) rotate(0)`)
|
||||
.attr("width", '1000px')
|
||||
.append("xhtml:div")
|
||||
.attr("class", "temporaryTextInside")
|
||||
.attr('style', () => {
|
||||
const styles: string[] = [];
|
||||
styles.push(`font-family: Noto Sans`)
|
||||
styles.push(`font-size: 11pt`);
|
||||
styles.push(`font-weight: bold`)
|
||||
styles.push(`color: #000000`);
|
||||
return styles.join("; ");
|
||||
})
|
||||
.attr("width", 'fit-content')
|
||||
.attr("height", 'max-content')
|
||||
.attr('contenteditable', 'true')
|
||||
.text("")
|
||||
.on("focusout", () => {
|
||||
let elem = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement;
|
||||
let savedText = elem.innerText;
|
||||
|
||||
let drawing = this.getDrawingMock("text", savedText);
|
||||
(drawing.element as TextElement).text = savedText;
|
||||
let svgText = this.mapDrawingToSvgConverter.convert(drawing);
|
||||
|
||||
this.drawingService
|
||||
.add(this.server, this.project.project_id, x, y, svgText)
|
||||
.subscribe((serverDrawing: Drawing) => {
|
||||
var temporaryElement = document.getElementById("temporaryText") as HTMLElement;
|
||||
temporaryElement.remove();
|
||||
this.drawingsDataSource.add(serverDrawing);
|
||||
});
|
||||
});
|
||||
|
||||
var elem = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement;
|
||||
elem.focus();
|
||||
this.resetDrawToolChoice();
|
||||
}
|
||||
|
||||
map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject);
|
||||
this.drawListener = addTextListener;
|
||||
map.addEventListener('click', this.drawListener as EventListenerOrEventListenerObject, {once : true});
|
||||
} else {
|
||||
this.resetDrawToolChoice();
|
||||
var map = document.getElementsByClassName('map')[0];
|
||||
map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject);
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.drawingsDataSource.clear();
|
||||
this.nodesDataSource.clear();
|
||||
|
Loading…
x
Reference in New Issue
Block a user