Initial implementation of text editing

This commit is contained in:
PiotrP 2018-12-07 09:10:57 -08:00
parent acac9edf0e
commit c0bfc3ed35
10 changed files with 245 additions and 43 deletions

View File

@ -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>`;

View File

@ -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> {
@ -24,4 +24,11 @@ export class ClickedDataEvent<T> {
public x: number,
public y: number
) {}
}
}
export class EditedDataEvent {
constructor(
public id: string,
public editedText: string
) {}
}

View File

@ -24,6 +24,8 @@ export class SelectionManager {
this.selection = dictItems;
if (selected.length > 0) {
//console.log(selected);
this.selected.emit(selected);
}

View File

@ -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;

View 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(){
}
}

View File

@ -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})`;
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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;

View File

@ -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();