ngx-emfular-diagram 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/ng-package.json +7 -0
- package/package.json +51 -0
- package/src/lib/components/arrows/arrow-between-boxes/arrow-between-boxes.component.css +0 -0
- package/src/lib/components/arrows/arrow-between-boxes/arrow-between-boxes.component.svg +12 -0
- package/src/lib/components/arrows/arrow-between-boxes/arrow-between-boxes.component.ts +60 -0
- package/src/lib/components/arrows/arrow-between-elems/arrow-between-elems.component.css +0 -0
- package/src/lib/components/arrows/arrow-between-elems/arrow-between-elems.component.svg +9 -0
- package/src/lib/components/arrows/arrow-between-elems/arrow-between-elems.component.ts +96 -0
- package/src/lib/components/arrows/arrow-between-points/arrow-between-points.component.css +0 -0
- package/src/lib/components/arrows/arrow-between-points/arrow-between-points.component.svg +15 -0
- package/src/lib/components/arrows/arrow-between-points/arrow-between-points.component.ts +39 -0
- package/src/lib/components/draggable/draggable.component.css +0 -0
- package/src/lib/components/draggable/draggable.component.svg +8 -0
- package/src/lib/components/draggable/draggable.component.ts +49 -0
- package/src/lib/components/rectangle/rectangle.component.css +0 -0
- package/src/lib/components/rectangle/rectangle.component.svg +3 -0
- package/src/lib/components/rectangle/rectangle.component.ts +14 -0
- package/src/lib/components/rectangle-with-text/rectangle-with-text.component.css +0 -0
- package/src/lib/components/rectangle-with-text/rectangle-with-text.component.svg +14 -0
- package/src/lib/components/rectangle-with-text/rectangle-with-text.component.ts +22 -0
- package/src/lib/components/text-area-svg/text-area-svg.component.css +0 -0
- package/src/lib/components/text-area-svg/text-area-svg.component.spec.ts +60 -0
- package/src/lib/components/text-area-svg/text-area-svg.component.svg +6 -0
- package/src/lib/components/text-area-svg/text-area-svg.component.ts +57 -0
- package/src/lib/models/arrow-style-configuration.ts +8 -0
- package/src/lib/models/bounding-box.ts +7 -0
- package/src/lib/models/dragger.ts +54 -0
- package/src/lib/models/identifiable.ts +3 -0
- package/src/lib/models/positionable.ts +8 -0
- package/src/lib/services/arrow-style-configuration.service.ts +25 -0
- package/src/lib/services/svg-access.service.ts +35 -0
- package/src/lib/utils/path-layouter.ts +41 -0
- package/src/lib/utils/position-helper.spec.ts +158 -0
- package/src/lib/utils/position-helper.ts +59 -0
- package/src/lib/utils/text-distributor.spec.ts +110 -0
- package/src/lib/utils/text-distributor.ts +68 -0
- package/src/public-api.ts +23 -0
- package/src/test-setup.ts +10 -0
- package/tsconfig.lib.json +18 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +13 -0
- package/vitest.config.ts +22 -0
- package/vitest.setup.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# EMFular-diagram
|
|
2
|
+
|
|
3
|
+
This project supplies components and services for the easier creation of ***svg-based*** diagrams.
|
|
4
|
+
To support such components, the library provides and uses explicit contracts of ***graphical identity*** (`interface Identifiable`) and ***position*** (`interface Positionable`), that using code should implement.
|
|
5
|
+
Diagram currently offers text alignment, drag-and-drop-support and arrow components that can attach to explicit points, bounding boxes, or identifiable objects.
|
|
6
|
+
Further contributions are welcome.
|
|
7
|
+
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
Install the library via `npm`:
|
|
11
|
+
```
|
|
12
|
+
npm install ngx-emfular-diagram
|
|
13
|
+
```
|
|
14
|
+
or adapt your package.json manually to the required version:
|
|
15
|
+
```
|
|
16
|
+
"ngx-emfular-diagram": "^1.0.0",
|
|
17
|
+
```
|
|
18
|
+
older versions are available under the old package name, `ngx-svg-graphics`. The latest old version is:
|
|
19
|
+
```
|
|
20
|
+
"ngx-svg-graphics": "^3.0.0",
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Support
|
|
24
|
+
Support is currently offered by the main developer, Susanne Göbel under goebel@uni-koblenz.de.
|
|
25
|
+
|
|
26
|
+
## Contributing
|
|
27
|
+
We are open to contributors. Maybe you would like to write your bachelor's or master's thesis on EMFular? Read our [arXiv-paper](https://arxiv.org/abs/2606.11442) and get in touch with Susanne Göbel goebel@uni-koblenz.de.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
EMFular-diagram is subject to (C) 2026, SoftLang Research Team, University of Koblenz, Faculty of CS, contact Susanne Göbel or Ralf Lämmel.
|
|
31
|
+
It is provided under the ***CC BY 4.0 license***.
|
|
32
|
+
Basically, you are free to share and adapt the material as long as you give proper credit to us and our project.
|
|
33
|
+
Feel free to include EMFular into your research but please cite us.
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ngx-emfular-diagram",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Small svg library to link components or svg elements with arrows and allow to drag components.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Drag",
|
|
7
|
+
"Arrows",
|
|
8
|
+
"SVG",
|
|
9
|
+
"Angular",
|
|
10
|
+
"EMF"
|
|
11
|
+
],
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Susanne Göbel",
|
|
14
|
+
"email": "goebel@uni-koblenz.de"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "ng-packagr -p ng-package.json",
|
|
18
|
+
"test": "vitest --config vitest.config.ts"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/softlang/EMFular-diagram"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@angular/cdk": "^19.2.7",
|
|
26
|
+
"@angular/common": "^19.2.7",
|
|
27
|
+
"@angular/core": "^19.2.7",
|
|
28
|
+
"@angular/forms": "^19.2.7",
|
|
29
|
+
"@types/uuid": "^10.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"tslib": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@analogjs/vite-plugin-angular": "^1.19.2",
|
|
37
|
+
"@angular-devkit/build-angular": "^19.2.25",
|
|
38
|
+
"@angular/build": "^19.2.25",
|
|
39
|
+
"@angular/compiler": "^19.2.7",
|
|
40
|
+
"@angular/platform-browser": "^19.2.7",
|
|
41
|
+
"@angular/platform-browser-dynamic": "^19.2.7",
|
|
42
|
+
"@testing-library/dom": "^10.4.1",
|
|
43
|
+
"@vitest/browser": "^4.1.10",
|
|
44
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
46
|
+
"ng-packagr": "^19.2.2",
|
|
47
|
+
"playwright": "^1.61.1",
|
|
48
|
+
"vite": "^8.1.5",
|
|
49
|
+
"vitest": "^4.1.10"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnChanges,} from '@angular/core';
|
|
2
|
+
import { Point } from "@angular/cdk/drag-drop";
|
|
3
|
+
import {v4 as uuidv4} from "uuid";
|
|
4
|
+
import {BoundingBox} from "../../../models/bounding-box";
|
|
5
|
+
import {PathLayouter} from "../../../utils/path-layouter";
|
|
6
|
+
import {ArrowBetweenPointsComponent} from "../arrow-between-points/arrow-between-points.component";
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: '[arrow-between-boxes]',
|
|
10
|
+
templateUrl: './arrow-between-boxes.component.svg',
|
|
11
|
+
styleUrl: './arrow-between-boxes.component.css',
|
|
12
|
+
imports: [ArrowBetweenPointsComponent]
|
|
13
|
+
})
|
|
14
|
+
export class ArrowBetweenBoxesComponent implements OnChanges, AfterViewInit {
|
|
15
|
+
@Input() start!: BoundingBox;
|
|
16
|
+
@Input() end!: BoundingBox;
|
|
17
|
+
@Input() arrowType?: string;
|
|
18
|
+
@Input() text?: string;
|
|
19
|
+
@Input() style?: string;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
x1: number = 0;
|
|
23
|
+
y1: number = 0;
|
|
24
|
+
x2: number = 5;
|
|
25
|
+
y2: number = 5;
|
|
26
|
+
|
|
27
|
+
id = uuidv4();
|
|
28
|
+
|
|
29
|
+
positioned= false;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private cdr: ChangeDetectorRef,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
ngAfterViewInit() {
|
|
36
|
+
this.computePositions()
|
|
37
|
+
this.positioned = true;
|
|
38
|
+
this.cdr.detectChanges();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ngOnChanges() {
|
|
42
|
+
if(this.positioned) {
|
|
43
|
+
this.computePositions()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
private computePositions() {
|
|
49
|
+
let res = PathLayouter.bestPoints(this.start, this.end);
|
|
50
|
+
this.applyBestPoints(res)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private applyBestPoints(res: Point[]) {
|
|
54
|
+
this.x1 = res[0].x;
|
|
55
|
+
this.y1 = res[0].y;
|
|
56
|
+
this.x2 = res[1].x;
|
|
57
|
+
this.y2 = res[1].y;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AfterViewInit,
|
|
3
|
+
ChangeDetectorRef,
|
|
4
|
+
Component, ElementRef,
|
|
5
|
+
Input, OnChanges, OnDestroy,
|
|
6
|
+
OnInit, SimpleChanges,
|
|
7
|
+
ViewChild
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import {Observable, Subscription} from "rxjs";
|
|
10
|
+
import { NgIf } from '@angular/common';
|
|
11
|
+
import {BoundingBox} from "../../../models/bounding-box";
|
|
12
|
+
import {ArrowBetweenBoxesComponent} from "../arrow-between-boxes/arrow-between-boxes.component";
|
|
13
|
+
import {SVGAccessService} from "../../../services/svg-access.service";
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: '[arrowElems]',
|
|
17
|
+
templateUrl: './arrow-between-elems.component.svg',
|
|
18
|
+
styleUrl: './arrow-between-elems.component.css',
|
|
19
|
+
imports: [NgIf, ArrowBetweenBoxesComponent]
|
|
20
|
+
})
|
|
21
|
+
export class ArrowBetweenElemsComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
|
|
22
|
+
|
|
23
|
+
@Input() startGID!: string;
|
|
24
|
+
@Input() startSuffix!: string;
|
|
25
|
+
@Input() endGID!: string;
|
|
26
|
+
@Input() endSuffix!: string;
|
|
27
|
+
@Input() arrowType?: string;
|
|
28
|
+
|
|
29
|
+
@Input() breaks: BoundingBox[] = [];
|
|
30
|
+
@Input() text?: string;
|
|
31
|
+
@Input() style?: string; //todo move into ArrowStyleConfig?
|
|
32
|
+
|
|
33
|
+
startId!: string;
|
|
34
|
+
endId!: string;
|
|
35
|
+
|
|
36
|
+
start?: BoundingBox;
|
|
37
|
+
end?: BoundingBox;
|
|
38
|
+
|
|
39
|
+
positioned= false;
|
|
40
|
+
@ViewChild('arrow') node!: ElementRef<SVGGraphicsElement>;
|
|
41
|
+
|
|
42
|
+
changeNotifier: Observable<string>;
|
|
43
|
+
changeSubscription: Subscription;
|
|
44
|
+
|
|
45
|
+
//idea: compute the two input positions as relative to the current elem
|
|
46
|
+
constructor(
|
|
47
|
+
private svgAccessService: SVGAccessService,
|
|
48
|
+
private cdr: ChangeDetectorRef) {
|
|
49
|
+
this.changeNotifier = this.svgAccessService.listenToPositionChange()
|
|
50
|
+
this.changeSubscription = this.changeNotifier.subscribe(nextString => {
|
|
51
|
+
if (nextString == this.startGID || nextString == this.endGID) {
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
this.computePositionsByIds()
|
|
54
|
+
this.cdr.detectChanges()
|
|
55
|
+
}, 0)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ngOnInit() {
|
|
61
|
+
this.startId = this.startGID+this.startSuffix;
|
|
62
|
+
this.endId = this.endGID+this.endSuffix;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ngOnChanges(_: SimpleChanges) {
|
|
66
|
+
this.startId = this.startGID+this.startSuffix;
|
|
67
|
+
this.endId = this.endGID+this.endSuffix;
|
|
68
|
+
this.computePositionsByIds()
|
|
69
|
+
this.cdr.detectChanges()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ngAfterViewInit() {
|
|
73
|
+
this.positioned = true;
|
|
74
|
+
this.computePositionsByIds()
|
|
75
|
+
this.cdr.detectChanges()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private computePositionsByIds() {
|
|
79
|
+
if (this.node?.nativeElement){
|
|
80
|
+
let rel = this.node.nativeElement as SVGGraphicsElement
|
|
81
|
+
let startOpt = this.svgAccessService.getRelativePosition(this.startId, rel)
|
|
82
|
+
if (startOpt) {
|
|
83
|
+
this.start = startOpt
|
|
84
|
+
}
|
|
85
|
+
let endOpt = this.svgAccessService.getRelativePosition(this.endId, rel)
|
|
86
|
+
if (endOpt) {
|
|
87
|
+
this.end = endOpt
|
|
88
|
+
}
|
|
89
|
+
} else console.log('No native element yet')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ngOnDestroy() {
|
|
93
|
+
this.changeSubscription.unsubscribe();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg:g>
|
|
2
|
+
<path [attr.id]="id+'-path'"
|
|
3
|
+
[attr.d]="'M '+startX+','+startY+' L '+endX+','+endY"
|
|
4
|
+
[attr.stroke]="arrowStyleConfiguration.color"
|
|
5
|
+
[attr.stroke-dasharray]="arrowStyleConfiguration.dashed"
|
|
6
|
+
[attr.marker-start]='"url(#"+arrowStyleConfiguration.startPointer+")"'
|
|
7
|
+
[attr.marker-end]='"url(#"+arrowStyleConfiguration.endPointer+")"'
|
|
8
|
+
style="{{style}}">
|
|
9
|
+
</path>
|
|
10
|
+
<text *ngIf="text">
|
|
11
|
+
<textPath [attr.href]="'#'+id+'-path'">
|
|
12
|
+
{{text}}
|
|
13
|
+
</textPath>
|
|
14
|
+
</text>
|
|
15
|
+
</svg:g>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {Component, Input, OnChanges} from '@angular/core';
|
|
2
|
+
import {NgIf} from "@angular/common";
|
|
3
|
+
import {ArrowStyleConfigurationService} from "../../../services/arrow-style-configuration.service";
|
|
4
|
+
import {ArrowStyleConfiguration} from "../../../models/arrow-style-configuration";
|
|
5
|
+
import {v4 as uuidv4} from "uuid";
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: '[arrow-between-points]',
|
|
9
|
+
imports: [
|
|
10
|
+
NgIf
|
|
11
|
+
],
|
|
12
|
+
templateUrl: './arrow-between-points.component.svg',
|
|
13
|
+
styleUrl: './arrow-between-points.component.css'
|
|
14
|
+
})
|
|
15
|
+
export class ArrowBetweenPointsComponent implements OnChanges {
|
|
16
|
+
|
|
17
|
+
@Input() startX!: number;
|
|
18
|
+
@Input() startY!: number;
|
|
19
|
+
@Input() endX!: number;
|
|
20
|
+
@Input() endY!: number;
|
|
21
|
+
@Input() text?: string;
|
|
22
|
+
@Input() style?: string;
|
|
23
|
+
|
|
24
|
+
@Input() arrowType?: string;
|
|
25
|
+
|
|
26
|
+
arrowStyleConfiguration: ArrowStyleConfiguration;
|
|
27
|
+
id = uuidv4();
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private arrowStyleConfigService: ArrowStyleConfigurationService,) {
|
|
31
|
+
|
|
32
|
+
this.arrowStyleConfiguration = this.arrowStyleConfigService.styleArrow()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ngOnChanges() {
|
|
36
|
+
this.arrowStyleConfiguration = this.arrowStyleConfigService.styleArrow(this.arrowType)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {AfterViewInit, Component, EventEmitter, Output} from '@angular/core';
|
|
2
|
+
import {SVGAccessService} from '../../services/svg-access.service';
|
|
3
|
+
import { Draggable } from '../../models/positionable';
|
|
4
|
+
import {Dragger} from "../../models/dragger";
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
imports: [],
|
|
8
|
+
selector: '[draggable]',
|
|
9
|
+
templateUrl: './draggable.component.svg',
|
|
10
|
+
styleUrl: './draggable.component.css'
|
|
11
|
+
})
|
|
12
|
+
export abstract class DraggableComponent<T extends Draggable> implements AfterViewInit {
|
|
13
|
+
|
|
14
|
+
@Output() chooseElem = new EventEmitter<T>();
|
|
15
|
+
//the caller must initialize both required elements (elem and elementDragger) either in the constructor
|
|
16
|
+
// (or if they are inputs) in the ngOnInit life cycle hook
|
|
17
|
+
elem!: T;
|
|
18
|
+
elemDragger!: Dragger<T>;
|
|
19
|
+
|
|
20
|
+
protected constructor(
|
|
21
|
+
protected svgAccessService: SVGAccessService
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ngAfterViewInit() {
|
|
26
|
+
this.svgAccessService.notifyPositionChange(this.elem.$gId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
startDrag(event: MouseEvent) {
|
|
30
|
+
this.elemDragger.startDrag(event);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
drag(event: MouseEvent) {
|
|
34
|
+
if (this.elemDragger.drag(event)) {
|
|
35
|
+
this.svgAccessService.notifyPositionChange(this.elem.$gId)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
endDrag(event: MouseEvent) {
|
|
40
|
+
this.elemDragger.endDrag(event);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clickElem(event: MouseEvent) {
|
|
44
|
+
if (this.elemDragger.clickElem(event)) {
|
|
45
|
+
this.chooseElem.emit(this.elem);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {Component, Input} from '@angular/core';
|
|
2
|
+
import {BoundingBox} from "../../models/bounding-box";
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: '[rectangleG]',
|
|
6
|
+
imports: [],
|
|
7
|
+
templateUrl: './rectangle.component.svg',
|
|
8
|
+
styleUrl: './rectangle.component.css'
|
|
9
|
+
})
|
|
10
|
+
export class RectangleComponent {
|
|
11
|
+
@Input() position!: BoundingBox
|
|
12
|
+
@Input() color?: string = '#ccffff';
|
|
13
|
+
|
|
14
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
|
2
|
+
import {BoundingBox} from "../../models/bounding-box";
|
|
3
|
+
import {RectangleComponent} from "../rectangle/rectangle.component";
|
|
4
|
+
import {TextAreaSvgComponent} from "../text-area-svg/text-area-svg.component";
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: '[rectangle-with-text]',
|
|
8
|
+
imports: [
|
|
9
|
+
TextAreaSvgComponent,
|
|
10
|
+
RectangleComponent
|
|
11
|
+
],
|
|
12
|
+
templateUrl: './rectangle-with-text.component.svg',
|
|
13
|
+
styleUrl: './rectangle-with-text.component.css'
|
|
14
|
+
})
|
|
15
|
+
export class RectangleWithTextComponent {
|
|
16
|
+
@Input() id!: string;
|
|
17
|
+
@Input() text!: string;
|
|
18
|
+
@Output() textChange = new EventEmitter<string>();
|
|
19
|
+
|
|
20
|
+
@Input() position!: BoundingBox
|
|
21
|
+
@Input() color?: string;
|
|
22
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
2
|
+
import {afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { TextAreaSvgComponent } from './text-area-svg.component';
|
|
5
|
+
|
|
6
|
+
describe('TextAreaSvgComponent', () => {
|
|
7
|
+
let component: TextAreaSvgComponent;
|
|
8
|
+
let fixture: ComponentFixture<TextAreaSvgComponent>;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await TestBed.configureTestingModule({
|
|
12
|
+
imports: [TextAreaSvgComponent]
|
|
13
|
+
}).compileComponents();
|
|
14
|
+
|
|
15
|
+
fixture = TestBed.createComponent(TextAreaSvgComponent);
|
|
16
|
+
component = fixture.componentInstance;
|
|
17
|
+
fixture.detectChanges();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
TestBed.resetTestingModule();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should create', () => {
|
|
25
|
+
expect(component).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function initializeDistributedText() {
|
|
29
|
+
component.x = 20;
|
|
30
|
+
component.y = 40;
|
|
31
|
+
component.h = 50;
|
|
32
|
+
component.w = 200;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function verifyDistributedText(text: string, distributedText: string[]) {
|
|
36
|
+
component.text = text;
|
|
37
|
+
component.distributeText();
|
|
38
|
+
expect(component.distributedText).toEqual(distributedText);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it('should set a two line distributed text', () => {
|
|
42
|
+
initializeDistributedText();
|
|
43
|
+
|
|
44
|
+
verifyDistributedText(
|
|
45
|
+
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam',
|
|
46
|
+
['Lorem ipsum dolor sit', 'amet, consetetur ...']
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should set a short distributed text after setting a longer one', () => {
|
|
51
|
+
initializeDistributedText();
|
|
52
|
+
|
|
53
|
+
verifyDistributedText(
|
|
54
|
+
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam',
|
|
55
|
+
['Lorem ipsum dolor sit', 'amet, consetetur ...']
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
verifyDistributedText('Short', ['Short']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg:text [attr.x]="x" [attr.y]="y" [attr.width]="w" [attr.height]="h" dy="0" [attr.style]="" (click)="handleClick()">
|
|
2
|
+
<tspan dy="1.2em" [attr.x]="x" *ngFor="let line of distributedText">{{line}}</tspan>
|
|
3
|
+
</svg:text>
|
|
4
|
+
<svg:foreignObject *ngIf="isActive" [attr.x]="x" [attr.y]="y" [attr.width]="w" [attr.height]="h">
|
|
5
|
+
<input id="text-area" type="text" [(ngModel)]="this.text" (focusout)="leaveTextInput()"/>
|
|
6
|
+
</svg:foreignObject>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
EventEmitter,
|
|
4
|
+
Input,
|
|
5
|
+
OnChanges,
|
|
6
|
+
Output,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { FormsModule } from '@angular/forms';
|
|
9
|
+
import { NgFor, NgIf } from '@angular/common';
|
|
10
|
+
import {TextDistributor} from "../../utils/text-distributor";
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: '[text-area-svg]',
|
|
14
|
+
templateUrl: './text-area-svg.component.svg',
|
|
15
|
+
styleUrl: './text-area-svg.component.css',
|
|
16
|
+
imports: [NgFor, NgIf, FormsModule]
|
|
17
|
+
})
|
|
18
|
+
export class TextAreaSvgComponent implements OnChanges {
|
|
19
|
+
/*
|
|
20
|
+
a fixed size svg. If the text exceeds the possible size, we will do a ... for now
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
@Input() text!: string
|
|
24
|
+
@Input() x!: number
|
|
25
|
+
@Input() y!: number
|
|
26
|
+
@Input() w!: number
|
|
27
|
+
@Input() h!: number
|
|
28
|
+
@Input() singleEdit: boolean = false
|
|
29
|
+
@Output() textChange = new EventEmitter<string>();
|
|
30
|
+
//only with singleEdit since that opens an overlay where one can change the text in place
|
|
31
|
+
|
|
32
|
+
distributedText: string[] = []
|
|
33
|
+
isActive: boolean = false;
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ngOnChanges() {
|
|
37
|
+
this.distributeText();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleClick() {
|
|
41
|
+
if(this.singleEdit) {
|
|
42
|
+
this.isActive = true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
leaveTextInput() {
|
|
47
|
+
this.textChange.emit(this.text)
|
|
48
|
+
this.isActive = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
distributeText(){
|
|
52
|
+
this.distributedText = TextDistributor.distributeText(
|
|
53
|
+
this.text, this.w, this.h
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {Positionable} from './positionable';
|
|
2
|
+
|
|
3
|
+
export class Dragger<T extends Positionable> {
|
|
4
|
+
elem: T;
|
|
5
|
+
|
|
6
|
+
dragActive = false;
|
|
7
|
+
wasReallyDragged = false;
|
|
8
|
+
dragStartX: number = 0;
|
|
9
|
+
dragStartY: number = 0;
|
|
10
|
+
|
|
11
|
+
constructor(element: T) {
|
|
12
|
+
this.elem = element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
startDrag(event: MouseEvent) {
|
|
16
|
+
this.dragStartX = event.clientX;
|
|
17
|
+
this.dragStartY = event.clientY;
|
|
18
|
+
this.dragActive = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// returns true in the case of a real drag event, false otherwise
|
|
22
|
+
drag(event: MouseEvent): boolean {
|
|
23
|
+
if (this.dragActive) {
|
|
24
|
+
this.wasReallyDragged = true;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
const dragX = event.clientX;
|
|
27
|
+
this.elem.position.x+= (dragX - this.dragStartX);
|
|
28
|
+
this.dragStartX = dragX;
|
|
29
|
+
const dragY = event.clientY;
|
|
30
|
+
this.elem.position.y+= (dragY - this.dragStartY);
|
|
31
|
+
this.dragStartY = dragY;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
endDrag(event: MouseEvent) {
|
|
38
|
+
this.dragActive = false;
|
|
39
|
+
// todo was working for click vs drag, not now setTimeout(() => {this.dragActive = false;}, 50);
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//returns true if the click should be treated as click, false if it was from drag
|
|
44
|
+
clickElem(event: MouseEvent): boolean {
|
|
45
|
+
if (this.wasReallyDragged) {
|
|
46
|
+
this.wasReallyDragged = false;
|
|
47
|
+
return false;
|
|
48
|
+
} else {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|