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 { RectElement } from '../../models/drawings/rect-element';
import { EllipseElement } from '../../models/drawings/ellipse-element'; import { EllipseElement } from '../../models/drawings/ellipse-element';
import { LineElement } from '../../models/drawings/line-element'; import { LineElement } from '../../models/drawings/line-element';
import { TextElement } from '../../models/drawings/text-element';
@Injectable() @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}\" />`; 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) { } 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}\" />` 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 ""; } else return "";
return `<svg height=\"${mapDrawing.element.height}\" width=\"${mapDrawing.element.width}\">${elem}</svg>`; 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 x: number,
public y: number, public y: number,
public width: number, public width: number,
public height: number) { public height: number
} ) {}
} }
export class ClickedDataEvent<T> { export class ClickedDataEvent<T> {
@ -24,4 +24,11 @@ export class ClickedDataEvent<T> {
public x: number, public x: number,
public y: 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; this.selection = dictItems;
if (selected.length > 0) { if (selected.length > 0) {
//console.log(selected);
this.selected.emit(selected); this.selected.emit(selected);
} }

View File

@ -29,6 +29,8 @@ export class SelectionTool {
private activate(selection) { private activate(selection) {
const self = this; const self = this;
console.log("test!!!", selection);
selection.on("mousedown", function() { selection.on("mousedown", function() {
const subject = select(window); const subject = select(window);
const parent = this.parentElement; 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] : []; 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 const drawing_enter = drawing
.enter() .enter()
.append<SVGTextElement>('text') .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); const merge = drawing.merge(drawing_enter);
merge merge
@ -75,6 +156,7 @@ export class TextDrawingWidget implements DrawingShapeWidget {
// approx and make it matching to GUI // approx and make it matching to GUI
const tspan = select(this).selectAll<SVGTSpanElement, string>('tspan'); const tspan = select(this).selectAll<SVGTSpanElement, string>('tspan');
const height = this.getBBox().height / tspan.size(); const height = this.getBBox().height / tspan.size();
//return `translate(0, ${height})`;
return `translate(${TextDrawingWidget.MARGIN}, ${height - TextDrawingWidget.MARGIN})`; return `translate(${TextDrawingWidget.MARGIN}, ${height - TextDrawingWidget.MARGIN})`;
}); });

View File

@ -23,7 +23,7 @@ g.node:hover {
background: transparent; background: transparent;
top: 20px; top: 20px;
left: 92px; left: 92px;
width: 232px !important; width: 296px !important;
height: 72px !important; height: 72px !important;
} }
@ -41,7 +41,7 @@ g.node:hover {
} }
.drawer { .drawer {
width: 232px !important; width: 296px !important;
height: 72px !important; height: 72px !important;
background:#263238; background:#263238;
} }
@ -51,7 +51,6 @@ g.node:hover {
} }
.drawer-button { .drawer-button {
height: 72px;
width: 64px!important; width: 64px!important;
background: #263238; background: #263238;
padding: 0; padding: 0;
@ -70,8 +69,7 @@ g.node:hover {
.drawer-arrow-button-left { .drawer-arrow-button-left {
width: 40px; width: 40px;
height: 72px; margin-left: 256px;
margin-left: 192px;
background:#263238; background:#263238;
position: fixed; position: fixed;
} }

View File

@ -102,6 +102,9 @@
<mat-drawer-container [ngClass]="{shadow: drawTools.visibility}" class="drawer-container"> <mat-drawer-container [ngClass]="{shadow: drawTools.visibility}" class="drawer-container">
<mat-drawer #drawer class="drawer"> <mat-drawer #drawer class="drawer">
<div class="drawer-buttons"> <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')"> <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> <mat-icon>crop_3_2</mat-icon>
</button> </button>

View File

@ -79,7 +79,7 @@ export class MockedDrawingsDataSource {
update() { return of({})} update() { return of({})}
} }
fdescribe('ProjectMapComponent', () => { describe('ProjectMapComponent', () => {
let component: ProjectMapComponent; let component: ProjectMapComponent;
let fixture: ComponentFixture<ProjectMapComponent>; let fixture: ComponentFixture<ProjectMapComponent>;
let drawingService = new MockedDrawingService; 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 { NodeContextMenu } from '../../cartography/events/nodes';
import { MapLinkCreated } from '../../cartography/events/links'; import { MapLinkCreated } from '../../cartography/events/links';
import { NodeWidget } from '../../cartography/widgets/node'; 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 { DrawingService } from '../../services/drawing.service';
import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-node-to-node-converter'; import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-node-to-node-converter';
import { NodesEventSource } from '../../cartography/events/nodes-event-source'; 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 { MapLabel } from '../../cartography/models/map/map-label';
import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component'; import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component';
import { MapLinkNode } from '../../cartography/models/map/map-link-node'; 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({ @Component({
@ -75,7 +78,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
'isRectangleChosen': false, 'isRectangleChosen': false,
'isEllipseChosen': false, 'isEllipseChosen': false,
'isLineChosen': false, 'isLineChosen': false,
'visibility': false 'visibility': false,
'isAddingTextChosen': false
}; };
protected selectedDrawing: string; protected selectedDrawing: string;
@ -344,6 +348,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}); });
} }
public onDrawingEdited(editedEvent: EditedDataEvent){
}
public set readonly(value) { public set readonly(value) {
this.inReadOnlyMode = value; this.inReadOnlyMode = value;
if (value) { if (value) {
@ -382,16 +390,19 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
switch (selectedObject) { switch (selectedObject) {
case "rectangle": case "rectangle":
this.drawTools.isAddingTextChosen = false;
this.drawTools.isEllipseChosen = false; this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen; this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen;
this.drawTools.isLineChosen = false; this.drawTools.isLineChosen = false;
break; break;
case "ellipse": case "ellipse":
this.drawTools.isAddingTextChosen = false;
this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen; this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen;
this.drawTools.isRectangleChosen = false; this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = false; this.drawTools.isLineChosen = false;
break; break;
case "line": case "line":
this.drawTools.isAddingTextChosen = false;
this.drawTools.isEllipseChosen = false; this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = false; this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = !this.drawTools.isLineChosen; this.drawTools.isLineChosen = !this.drawTools.isLineChosen;
@ -424,6 +435,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.drawTools.isRectangleChosen = false; this.drawTools.isRectangleChosen = false;
this.drawTools.isEllipseChosen = false; this.drawTools.isEllipseChosen = false;
this.drawTools.isLineChosen = false; this.drawTools.isLineChosen = false;
this.drawTools.isAddingTextChosen = false;
this.selectedDrawing = ""; this.selectedDrawing = "";
} }
@ -438,45 +450,58 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.drawTools.visibility = true; this.drawTools.visibility = true;
} }
public getDrawingMock(objectType: string): MapDrawing { public getDrawingMock(objectType: string, text?: string): MapDrawing {
let drawingElement: DrawingElement; let drawingElement: DrawingElement;
switch (objectType) { switch (objectType) {
case "rectangle": case "rectangle":
let rect = new RectElement(); let rectElement = new RectElement();
rect.fill = "#ffffff"; rectElement.fill = "#ffffff";
rect.fill_opacity = 1.0; rectElement.fill_opacity = 1.0;
rect.stroke = "#000000"; rectElement.stroke = "#000000";
rect.stroke_width = 2; rectElement.stroke_width = 2;
rect.width = 200; rectElement.width = 200;
rect.height = 100; rectElement.height = 100;
drawingElement = rect; drawingElement = rectElement;
break; break;
case "ellipse": case "ellipse":
let ellipse = new EllipseElement(); let ellipseElement = new EllipseElement();
ellipse.fill = "#ffffff"; ellipseElement.fill = "#ffffff";
ellipse.fill_opacity = 1.0; ellipseElement.fill_opacity = 1.0;
ellipse.stroke = "#000000"; ellipseElement.stroke = "#000000";
ellipse.stroke_width = 2; ellipseElement.stroke_width = 2;
ellipse.cx = 100; ellipseElement.cx = 100;
ellipse.cy = 100; ellipseElement.cy = 100;
ellipse.rx = 100; ellipseElement.rx = 100;
ellipse.ry = 100; ellipseElement.ry = 100;
ellipse.width = 200; ellipseElement.width = 200;
ellipse.height = 200; ellipseElement.height = 200;
drawingElement = ellipse; drawingElement = ellipseElement;
break; break;
case "line": case "line":
let line = new LineElement(); let lineElement = new LineElement();
line.stroke = "#000000"; lineElement.stroke = "#000000";
line.stroke_width = 2; lineElement.stroke_width = 2;
line.x1 = 0; lineElement.x1 = 0;
line.x2 = 200; lineElement.x2 = 200;
line.y1 = 0; lineElement.y1 = 0;
line.y2 = 0; lineElement.y2 = 0;
line.width = 100; lineElement.width = 100;
line.height = 0; lineElement.height = 0;
drawingElement = line; 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(); let mapDrawing = new MapDrawing();
@ -484,6 +509,68 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
return mapDrawing; 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() { public ngOnDestroy() {
this.drawingsDataSource.clear(); this.drawingsDataSource.clear();
this.nodesDataSource.clear(); this.nodesDataSource.clear();