ngx-emfular-tool 1.0.0 → 1.1.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.
Files changed (38) hide show
  1. package/.github/workflows/cd.yml +64 -0
  2. package/.github/workflows/ci.yml +34 -0
  3. package/README.md +68 -38
  4. package/ng-package.json +7 -0
  5. package/package.json +23 -20
  6. package/src/lib/alerting/alert/alert.component.css +18 -0
  7. package/src/lib/alerting/alert/alert.component.html +7 -0
  8. package/src/lib/alerting/alert/alert.component.spec.ts +41 -0
  9. package/src/lib/alerting/alert/alert.component.ts +21 -0
  10. package/src/lib/alerting/alert.service.spec.ts +64 -0
  11. package/src/lib/alerting/alert.service.ts +28 -0
  12. package/src/lib/history/history.service.spec.ts +260 -0
  13. package/src/lib/history/history.service.ts +164 -0
  14. package/src/lib/io/input-handler.spec.ts +26 -0
  15. package/src/lib/io/input-handler.ts +15 -0
  16. package/src/lib/io/io.service.spec.ts +147 -0
  17. package/src/lib/io/io.service.ts +129 -0
  18. package/src/lib/modal/modal-instance.ts +7 -0
  19. package/src/lib/modal/modal-ref.spec.ts +110 -0
  20. package/src/lib/modal/modal-ref.ts +23 -0
  21. package/src/lib/modal/modal.service.spec.ts +92 -0
  22. package/src/lib/modal/modal.service.ts +31 -0
  23. package/src/public-api.ts +14 -0
  24. package/src/test-setup.ts +10 -0
  25. package/tsconfig.lib.json +18 -0
  26. package/tsconfig.lib.prod.json +11 -0
  27. package/tsconfig.spec.json +13 -0
  28. package/vitest.config.ts +23 -0
  29. package/vitest.setup.ts +6 -0
  30. package/fesm2022/ngx-emfular-tool.mjs +0 -333
  31. package/fesm2022/ngx-emfular-tool.mjs.map +0 -1
  32. package/index.d.ts +0 -5
  33. package/lib/alerting/alert/alert.component.d.ts +0 -10
  34. package/lib/alerting/alert.service.d.ts +0 -10
  35. package/lib/history/history.service.d.ts +0 -29
  36. package/lib/io/input-handler.d.ts +0 -4
  37. package/lib/io/io.service.d.ts +0 -17
  38. package/public-api.d.ts +0 -5
@@ -0,0 +1,64 @@
1
+ name: CD
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ release:
14
+ name: Publish package
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Verify tag is on main
21
+ run: |
22
+ git fetch origin main
23
+
24
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
25
+ echo "You may only create releases from main."
26
+ echo "The tagged commit $GITHUB_SHA is not part of origin/main."
27
+ exit 1
28
+ fi
29
+
30
+ echo "Tag points to a commit on main"
31
+
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version: 24
35
+ registry-url: https://registry.npmjs.org
36
+
37
+ - run: npm ci
38
+
39
+ - name: Install Playwright browsers (for vitest browser tests)
40
+ run: npx playwright install --with-deps chromium
41
+
42
+ - name: test
43
+ run: npm run test
44
+
45
+ - run: npm run build
46
+
47
+ - name: Check package version
48
+ run: |
49
+ VERSION=$(node -p "require('./package.json').version")
50
+ TAG=${GITHUB_REF#refs/tags/v}
51
+
52
+ if [ "$VERSION" != "$TAG" ]; then
53
+ echo "Version mismatch!"
54
+ echo "package.json contains version $VERSION"
55
+ echo "but the tag is v$TAG"
56
+ exit 1
57
+ fi
58
+ echo "Versions match: $VERSION"
59
+
60
+ - name: Publish
61
+ run: npm publish --provenance --access public
62
+
63
+ - name: Create GitHub Release
64
+ uses: softprops/action-gh-release@v2
@@ -0,0 +1,34 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ push:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ test:
11
+ name: Test and package library
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Node
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 22
22
+ cache: npm
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Install Playwright browsers (for vitest browser tests)
28
+ run: npx playwright install --with-deps chromium
29
+
30
+ - name: Run tests
31
+ run: npm run test
32
+
33
+ - name: Build library
34
+ run: npm run build
package/README.md CHANGED
@@ -1,63 +1,93 @@
1
- # NgxEmfularHelper
1
+ # EMFular-Tool
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.0.
3
+ This project supplies model-agnostic Angular utilities for building EMFular editors.
4
+ In the EMFular megamodel, Tool is the layer for editor functionality that is not part of the model semantics of `EMFular-Core` and not part of the SVG rendering contracts of `EMFular-Diagram`.
5
+ It provides browser-side facilities for history, file input/output, export, input normalization, and alert dialogs.
4
6
 
5
- ## Code scaffolding
7
+ ## Concepts
6
8
 
7
- Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
9
+ The EMFular megamodel identifies three Tool concepts: ***Graphical-File Management***, ***JSON-History Management***, and ***JSON-File Management***.
10
+ They map to the code as follows.
8
11
 
9
- ```bash
10
- ng generate component component-name
11
- ```
12
+ ### Graphical-File Management
13
+
14
+ Graphical-file management is implemented by the SVG export part of `IoService`.
15
+ It receives an `SVGElement`, removes Angular runtime artifacts from a cloned SVG, serializes the SVG, and saves it either as SVG directly or as PNG/JPEG through an intermediate canvas.
16
+
17
+ Code sections:
12
18
 
13
- For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
19
+ - `projects/tool/src/lib/io/io.service.ts`: `cleanCopySVGAsBlob`, `saveSVG`, `saveSvgAsPng`, `saveSvgAsJpeg`
20
+ - `projects/tool/src/lib/io/io.service.ts`: private conversion helpers `convertSvgBlobToPngOrJpegAndDownload`, `loadImgToCanvas`, `canvasToPng`, `canvasToJpeg`
14
21
 
15
- ```bash
16
- ng generate --help
22
+ ```ts
23
+ export class IoService {
24
+ cleanCopySVGAsBlob(svg: SVGElement): Blob;
25
+ saveSVG(svgContent: SVGElement, title: string): void;
26
+ saveSvgAsPng(svgContent: SVGElement, title: string): void;
27
+ saveSvgAsJpeg(svgContent: SVGElement, title: string): void;
28
+ }
17
29
  ```
18
30
 
19
- ## Building
31
+ ### JSON-History Management
20
32
 
21
- To build the library, run:
33
+ JSON-history management is implemented by `HistoryService<T>`.
34
+ The service is generic, but in an EMFular editor it is normally instantiated as `HistoryService<JsonOf<M>>`, so each history entry is the JSON representation of a model state.
35
+ The states are stored in a circular `localStorage` buffer and exposed through `state$` for undo, redo, and session recovery.
22
36
 
23
- ```bash
24
- ng build tool
25
- ```
37
+ Code sections:
38
+
39
+ - `projects/tool/src/lib/history/history.service.ts`: circular history buffer, `state$`, `save`, `undo`, `redo`, `clearHistory`
26
40
 
27
- This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
41
+ ```ts
42
+ export class HistoryService<T> {
43
+ state$: Observable<T | null>;
28
44
 
29
- ### Publishing the Library
45
+ constructor(prefix?: string, bufferSize?: number, platformId?: Object);
30
46
 
31
- Once the project is built, you can publish your library by following these steps:
47
+ clearHistory(): void;
48
+ save(elem: T): void;
49
+ undo(): T | null;
50
+ redo(): T | null;
51
+ isUndoNotPossible(): boolean;
52
+ isRedoNotPossible(): boolean;
53
+ }
54
+ ```
32
55
 
33
- 1. Navigate to the `dist` directory:
34
- ```bash
35
- cd dist/tool
36
- ```
56
+ ### JSON-File Management
37
57
 
38
- 2. Run the `npm publish` command to publish your library to the npm registry:
39
- ```bash
40
- npm publish
41
- ```
58
+ JSON-file management is split between Tool and Integration.
59
+ Tool provides the browser file primitives in `IoService`: reading a selected file as text and saving a JSON string as a `.json` download.
42
60
 
43
- ## Running unit tests
61
+ Code sections:
44
62
 
45
- To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
63
+ - `projects/tool/src/lib/io/io.service.ts`: `loadStringFromFile`, `saveJson`, `saveFile`
46
64
 
47
- ```bash
48
- ng test
65
+ ```ts
66
+ export class IoService {
67
+ loadStringFromFile(event: Event): Promise<string>;
68
+ saveFile(contentBlob: Blob, fileName: string): void;
69
+ saveJson(json: string, title: string): void;
70
+ }
49
71
  ```
50
72
 
51
- ## Running end-to-end tests
73
+ ### Additional Tool Helpers
52
74
 
53
- For end-to-end (e2e) testing, run:
75
+ The package also exports small helper APIs that are not separate Tool concepts in the megamodel:
54
76
 
55
- ```bash
56
- ng e2e
57
- ```
77
+ - `InputHandler` normalizes browser input events, especially numeric input and repeated file selections.
78
+ - `AlertService` and `AlertComponent` provide a minimal CDK-based alerting via pop-ups.
79
+
80
+ ## Support
81
+
82
+ Support is currently offered by the main developer, Susanne Göbel under goebel@uni-koblenz.de.
83
+
84
+ ## Contributing
58
85
 
59
- Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
86
+ 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.
60
87
 
61
- ## Additional Resources
88
+ ## License
62
89
 
63
- For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
90
+ EMFular-diagram is subject to (C) 2026, SoftLang Research Team, University of Koblenz, Faculty of CS, contact Susanne Göbel or Ralf Lämmel.
91
+ It is provided under the ***CC BY 4.0 license***.
92
+ Basically, you are free to share and adapt the material as long as you give proper credit to us and our project.
93
+ Feel free to include EMFular into your research but please cite us.
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "dist",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "ngx-emfular-tool",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Small Angular library with helper schematics for EMFular, currently alerting, history, and io facilities",
5
- "keywords": [
6
- "EMFular",
7
- "Angular",
8
- "IO",
9
- "alert"
10
- ],
5
+ "keywords": ["EMFular","Angular", "IO", "alert"],
11
6
  "author": {
12
7
  "name": "Susanne Göbel",
13
8
  "email": "goebel@uni-koblenz.de"
@@ -16,27 +11,35 @@
16
11
  "type": "git",
17
12
  "url": "https://github.com/softlang/EMFular-tool"
18
13
  },
14
+ "scripts": {
15
+ "build": "ng-packagr -p ng-package.json",
16
+ "test": "vitest --config vitest.config.ts"
17
+ },
19
18
  "peerDependencies": {
20
19
  "@angular/cdk": "^19.2.7",
21
20
  "@angular/common": "^19.2.7",
22
21
  "@angular/core": "^19.2.7",
23
22
  "@angular/forms": "^19.2.7",
24
- "@types/uuid": "^10.0.0",
25
- "@angular/material": "^19.2.7"
23
+ "@types/uuid": "^10.0.0"
26
24
  },
27
25
  "dependencies": {
28
26
  "tslib": "^2.3.0"
29
27
  },
30
28
  "sideEffects": false,
31
- "module": "fesm2022/ngx-emfular-tool.mjs",
32
- "typings": "index.d.ts",
33
- "exports": {
34
- "./package.json": {
35
- "default": "./package.json"
36
- },
37
- ".": {
38
- "types": "./index.d.ts",
39
- "default": "./fesm2022/ngx-emfular-tool.mjs"
40
- }
29
+ "devDependencies": {
30
+ "@analogjs/vite-plugin-angular": "^1.19.2",
31
+ "@angular-devkit/build-angular": "^19.2.25",
32
+ "@angular/build": "^19.2.25",
33
+ "@angular/compiler": "^19.2.7",
34
+ "@angular/platform-browser": "^19.2.7",
35
+ "@angular/platform-browser-dynamic": "^19.2.7",
36
+ "@testing-library/dom": "^10.4.1",
37
+ "@vitest/browser": "^4.1.10",
38
+ "@vitest/browser-playwright": "^4.1.10",
39
+ "jsdom": "^29.1.1",
40
+ "ng-packagr": "^19.2.2",
41
+ "playwright": "^1.61.1",
42
+ "vite": "^8.1.5",
43
+ "vitest": "^4.1.10"
41
44
  }
42
- }
45
+ }
@@ -0,0 +1,18 @@
1
+ .tool-button-text{
2
+ margin: 1rem;
3
+ text-align: center;
4
+ vertical-align: middle;
5
+ color: orange;
6
+ height: 3rem;
7
+ width: 6rem;
8
+ background: white;
9
+ border: 2px solid orange;
10
+ border-radius: 4px;
11
+ }
12
+ .tool-button-text:hover {
13
+ box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
14
+ }
15
+
16
+ .p {
17
+
18
+ }
@@ -0,0 +1,7 @@
1
+ <div style="text-align: center">
2
+ <h3>Warning</h3>
3
+ <p>{{message}}</p>
4
+ <button class="tool-button-text" (click)="closeMe()">
5
+ Ok
6
+ </button>
7
+ </div>
@@ -0,0 +1,41 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { AlertComponent } from './alert.component';
5
+ import { OverlayRef } from '@angular/cdk/overlay';
6
+
7
+ describe('AlertComponent', () => {
8
+ let component: AlertComponent;
9
+ let fixture: ComponentFixture<AlertComponent>;
10
+ let overlayRef: OverlayRef;
11
+
12
+ beforeEach(async () => {
13
+ overlayRef = {
14
+ dispose: vi.fn()
15
+ } as unknown as OverlayRef;
16
+
17
+ await TestBed.configureTestingModule({
18
+ imports: [AlertComponent],
19
+ providers: [
20
+ { provide: OverlayRef, useValue: overlayRef }
21
+ ]
22
+ }).compileComponents();
23
+
24
+ fixture = TestBed.createComponent(AlertComponent);
25
+ component = fixture.componentInstance;
26
+ fixture.detectChanges();
27
+ });
28
+
29
+ afterEach(() => {
30
+ TestBed.resetTestingModule();
31
+ });
32
+
33
+ it('should create', () => {
34
+ expect(component).toBeTruthy();
35
+ });
36
+
37
+ it('closes the overlay', () => {
38
+ component.closeMe();
39
+ expect(overlayRef.dispose).toHaveBeenCalled();
40
+ });
41
+ });
@@ -0,0 +1,21 @@
1
+ import { Component } from '@angular/core';
2
+ import {OverlayRef} from "@angular/cdk/overlay";
3
+
4
+ @Component({
5
+ selector: 'alert-component',
6
+ imports: [],
7
+ templateUrl: './alert.component.html',
8
+ styleUrl: './alert.component.css'
9
+ })
10
+ export class AlertComponent {
11
+
12
+ public message!: string;
13
+
14
+ constructor(
15
+ private readonly overlayRef: OverlayRef
16
+ ) {}
17
+
18
+ closeMe(): void {
19
+ this.overlayRef.dispose();
20
+ }
21
+ }
@@ -0,0 +1,64 @@
1
+ import {describe, expect, it, vi} from 'vitest';
2
+
3
+ import {AlertService} from './alert.service';
4
+ import {AlertComponent} from './alert/alert.component';
5
+ import {ModalService} from '../modal/modal.service';
6
+ import {ModalInstance} from '../modal/modal-instance';
7
+ import {ModalRef} from '../modal/modal-ref';
8
+
9
+ describe('AlertService', () => {
10
+
11
+ function createModalInstance(): ModalInstance<AlertComponent, void> {
12
+ return {
13
+ ref: {} as ModalRef<void>,
14
+ componentRef: {
15
+ instance: {} as AlertComponent
16
+ } as any
17
+ };
18
+ }
19
+
20
+ it('creates an alert with the given message', () => {
21
+ const modalInstance = createModalInstance();
22
+ const modalService = {
23
+ createModal: vi.fn(() => modalInstance)
24
+ } as unknown as ModalService;
25
+
26
+ const service = new AlertService(modalService);
27
+ service.alert('Hello');
28
+ expect(modalService.createModal).toHaveBeenCalledWith(
29
+ AlertComponent,
30
+ {
31
+ width: '20%',
32
+ height: '30%',
33
+ hasBackdrop: true,
34
+ backdropClass: 'cdk-overlay-dark-backdrop',
35
+ panelClass: 'alert-overlay'
36
+ }
37
+ );
38
+ expect(modalInstance.componentRef.instance.message)
39
+ .toBe('Hello');
40
+ });
41
+
42
+ it('creates a separate modal for each alert', () => {
43
+ const first = createModalInstance();
44
+ const second = createModalInstance();
45
+
46
+ const modalService = {
47
+ createModal: vi.fn()
48
+ .mockReturnValueOnce(first)
49
+ .mockReturnValueOnce(second)
50
+ } as unknown as ModalService;
51
+
52
+ const service = new AlertService(modalService);
53
+ service.alert('First');
54
+ service.alert('Second');
55
+
56
+ expect(modalService.createModal).toHaveBeenCalledTimes(2);
57
+ expect(first.componentRef.instance.message)
58
+ .toBe('First');
59
+ expect(second.componentRef.instance.message)
60
+ .toBe('Second');
61
+ expect(first).not.toBe(second);
62
+ });
63
+
64
+ });
@@ -0,0 +1,28 @@
1
+ import { Injectable } from '@angular/core';
2
+ import {AlertComponent} from "./alert/alert.component";
3
+ import {ModalService} from "../modal/modal.service";
4
+ import {ModalInstance} from "../modal/modal-instance";
5
+
6
+ @Injectable({
7
+ providedIn: 'root'
8
+ })
9
+ export class AlertService {
10
+
11
+ constructor(
12
+ private readonly modalService: ModalService
13
+ ) {}
14
+
15
+ alert(msg: string): void {
16
+ const modalInstance: ModalInstance<AlertComponent, void> = this.modalService.createModal<AlertComponent, void>(
17
+ AlertComponent,
18
+ {
19
+ width: '20%',
20
+ height: '30%',
21
+ hasBackdrop: true,
22
+ backdropClass: 'cdk-overlay-dark-backdrop',
23
+ panelClass: 'alert-overlay'
24
+ }
25
+ );
26
+ modalInstance.componentRef.instance.message = msg;
27
+ }
28
+ }