From 2fb3be636b055855115ea1ac8253d19fee620549 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Wed, 7 Jul 2021 16:07:03 +0200
Subject: [PATCH] [editor] Add service for handling ID generation

Also keep track of already given ones.
---
 .../properties/properties.component.html      |  5 +-
 .../properties/properties.component.ts        | 10 +++-
 projects/editor/src/app/id.service.ts         | 57 +++++++++++++++++++
 projects/editor/src/app/model/UnitFactory.ts  | 19 +------
 projects/editor/src/app/unit.service.ts       | 26 +++++++--
 5 files changed, 89 insertions(+), 28 deletions(-)
 create mode 100644 projects/editor/src/app/id.service.ts

diff --git a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html
index 101dee255..d55c3ff8c 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html
+++ b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html
@@ -8,8 +8,9 @@
       <div fxLayout="column">
         <mat-form-field *ngIf="combinedProperties.hasOwnProperty('id')">
           <mat-label>ID</mat-label>
-          <input matInput type="text" [value]="combinedProperties.id"
-                 (input)="updateModel('id', $any($event.target).value)">
+          <input matInput type="text" *ngIf="selectedElements.length === 1" [value]="combinedProperties.id"
+                 (input)="updateModel('id', $any($event.target).value, $event)">
+          <input matInput type="text" disabled *ngIf="selectedElements.length > 1" [value]="'Muss eindeutig sein'">
         </mat-form-field>
         <mat-form-field *ngIf="combinedProperties.hasOwnProperty('label')">
           <mat-label>Label</mat-label>
diff --git a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts
index 611cb31cb..c1849dd83 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts
@@ -52,8 +52,14 @@ export class PropertiesComponent {
     }
   }
 
-  updateModel(property: string, value: string | number | boolean): void {
-    this.unitService.updateSelectedElementProperty(property, value);
+  // event as optional parameter in case the input is invalid and the old value needs
+  // to be restored. This is for now only relevant for IDs. Might need rework for other properties.
+  updateModel(property: string, value: string | number | boolean, event?: Event): void {
+    if (!this.unitService.updateSelectedElementProperty(property, value)) {
+      if (event) {
+        (event.target as HTMLInputElement).value = <string> this.combinedProperties[property];
+      }
+    }
   }
 
   deleteElement(): void {
diff --git a/projects/editor/src/app/id.service.ts b/projects/editor/src/app/id.service.ts
new file mode 100644
index 000000000..da521b21d
--- /dev/null
+++ b/projects/editor/src/app/id.service.ts
@@ -0,0 +1,57 @@
+import { Injectable } from '@angular/core';
+import {
+  Unit, UnitPage, UnitPageSection, UnitUIElement
+} from '../../../common/unit';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class IdService {
+  private givenIDs: string[] = [];
+  private idCounter: Record<string, number> = {
+    label: 0,
+    button: 0,
+    'text-field': 0,
+    checkbox: 0,
+    dropdown: 0,
+    radio: 0,
+    image: 0,
+    audio: 0,
+    video: 0,
+    correction: 0
+  };
+
+  getNewID(type: string): string {
+    do {
+      this.idCounter[type] += 1;
+    }
+    while (!this.isIdAvailable(`${type}_${this.idCounter[type]}`));
+    this.givenIDs.push(`${type}_${this.idCounter[type]}`);
+    return `${type}_${this.idCounter[type]}`;
+  }
+
+  readExistingIDs(unit: Unit): void {
+    unit.pages.forEach((page: UnitPage) => {
+      page.sections.forEach((section: UnitPageSection) => {
+        section.elements.forEach((element: UnitUIElement) => {
+          this.givenIDs.push(element.id);
+        });
+      });
+    });
+  }
+
+  isIdAvailable(value: string): boolean {
+    return !this.givenIDs.includes(value);
+  }
+
+  addId(id: string): void {
+    this.givenIDs.push(id);
+  }
+
+  removeId(id: string): void {
+    const index = this.givenIDs.indexOf(id, 0);
+    if (index > -1) {
+      this.givenIDs.splice(index, 1);
+    }
+  }
+}
diff --git a/projects/editor/src/app/model/UnitFactory.ts b/projects/editor/src/app/model/UnitFactory.ts
index d3d8c73bf..332e68b17 100644
--- a/projects/editor/src/app/model/UnitFactory.ts
+++ b/projects/editor/src/app/model/UnitFactory.ts
@@ -6,23 +6,6 @@ import {
   VideoElement
 } from '../../../../common/unit';
 
-const idCounter: Record<string, number> = {
-  label: 0,
-  button: 0,
-  'text-field': 0,
-  checkbox: 0,
-  dropdown: 0,
-  radio: 0,
-  image: 0,
-  audio: 0,
-  video: 0,
-  correction: 0
-};
-
-function getNewID(type: string): string {
-  idCounter[type] += 1;
-  return `${type}_${idCounter[type]}`;
-}
 
 export function createUnit(): Unit {
   return {
@@ -51,7 +34,7 @@ export function createUnitPageSection(): UnitPageSection {
 export function createUnitUIElement(type: string): UnitUIElement {
   return {
     type,
-    id: getNewID(type),
+    id: 'id_placeholder',
     xPosition: 0,
     yPosition: 0,
     width: 180,
diff --git a/projects/editor/src/app/unit.service.ts b/projects/editor/src/app/unit.service.ts
index 42a851b94..e4f467504 100644
--- a/projects/editor/src/app/unit.service.ts
+++ b/projects/editor/src/app/unit.service.ts
@@ -3,11 +3,12 @@ import {
   BehaviorSubject, Observable, Subject
 } from 'rxjs';
 import {
-  Unit, UnitPage, UnitUIElement
+  Unit, UnitPage, UnitPageSection, UnitUIElement
 } from '../../../common/unit';
 import { FileService } from '../../../common/file.service';
 import * as UnitFactory from './model/UnitFactory';
 import { MessageService } from '../../../common/message.service';
+import { IdService } from './id.service';
 
 @Injectable({
   providedIn: 'root'
@@ -22,7 +23,7 @@ export class UnitService {
   pageSwitch = new Subject();
   elementUpdated = new Subject();
 
-  constructor(private messageService: MessageService) {
+  constructor(private messageService: MessageService, private idService: IdService) {
     this._unit = new BehaviorSubject(UnitFactory.createUnit());
     const initialPage = UnitFactory.createUnitPage();
     const initialSection = UnitFactory.createUnitPageSection();
@@ -133,8 +134,10 @@ export class UnitService {
       case 'correction':
         newElement = UnitFactory.createCorrectionElement();
         break;
-      // no default
+      default:
+        throw new Error(`ElementType ${elementType} not found!`);
     }
+    newElement.id = this.idService.getNewID(elementType);
     this._unit.value.pages[this._selectedPageIndex.value]
       .sections[this._selectedPageSectionIndex.value].elements.push(newElement!);
 
@@ -159,15 +162,24 @@ export class UnitService {
     this.elementUpdated.next();
   }
 
-  updateSelectedElementProperty(property: string, value: string | number | boolean): void {
-    this._selectedElements.value.forEach((element: UnitUIElement) => {
+  updateSelectedElementProperty(property: string, value: string | number | boolean): boolean {
+    for (const element of this._selectedElements.value) {
       if (['string', 'number', 'boolean'].indexOf(typeof element[property]) > -1) {
+        if (property === 'id') {
+          if (!this.idService.isIdAvailable((value as string))) { // prohibit existing IDs
+            this.messageService.showError('ID ist bereits vergeben');
+            return false;
+          }
+          this.idService.removeId(element[property]);
+          this.idService.addId(<string>value);
+        }
         element[property] = value;
       } else if (Array.isArray(element[property])) {
         (element[property] as string[]).push(value as string);
       }
-    });
+    }
     this.elementUpdated.next();
+    return true;
   }
 
   deleteSelectedElements(): void {
@@ -197,6 +209,8 @@ export class UnitService {
     this._unit.value.pages.forEach((page: UnitPage) => {
       this._pages.push(new BehaviorSubject(page));
     });
+
+    this.idService.readExistingIDs(this._unit.value);
   }
 
   selectPageSection(index: number): void {
-- 
GitLab