From aa6583bf6d1116c2b7d73818ad9eba3e55a61eb7 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Mon, 20 Dec 2021 10:12:17 +0100
Subject: [PATCH] [player] Implement UnitStateElementMapperService

- Map value between unit state and element model
- Takes over methods that were previously present in element container
- Gets and holds a list for mapping drop list elements
---
 docs/release-notes-player.txt                 |  1 +
 projects/player/src/app/app.component.ts      | 16 ++--
 .../element-container.component.ts            | 87 +++++++-----------
 .../unit-state-element-mapper.service.ts      | 91 +++++++++++++++++++
 4 files changed, 133 insertions(+), 62 deletions(-)
 create mode 100644 projects/player/src/app/services/unit-state-element-mapper.service.ts

diff --git a/docs/release-notes-player.txt b/docs/release-notes-player.txt
index 85f557e8e..ebd33b3fc 100644
--- a/docs/release-notes-player.txt
+++ b/docs/release-notes-player.txt
@@ -5,6 +5,7 @@ Player
  - Use fixed size property for video and spelling components
  - Color the disabled progress bar of the media player green
  - Fix the section from being placed in front of elements with negative z-index
+ - Fix saving of drop list elements
 
 1.12.0
 - Use fixed size property for dynamic button, drop-list and text-field components
diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts
index b2ef82c2b..ccb97b70a 100644
--- a/projects/player/src/app/app.component.ts
+++ b/projects/player/src/app/app.component.ts
@@ -1,19 +1,18 @@
 import { Component, OnInit } from '@angular/core';
-import { TranslateService } from '@ngx-translate/core';
-import { MatDialog } from '@angular/material/dialog';
 import { registerLocaleData } from '@angular/common';
 import localeDe from '@angular/common/locales/de';
-import {
-  Unit
-} from '../../../common/models/unit';
+import { MatDialog } from '@angular/material/dialog';
+import { TranslateService } from '@ngx-translate/core';
+import { Unit } from '../../../common/models/unit';
+import { PlayerConfig, VopStartCommand } from './models/verona';
+import { UnitStateElementMapperService } from './services/unit-state-element-mapper.service';
 import { VeronaSubscriptionService } from './services/verona-subscription.service';
 import { VeronaPostService } from './services/verona-post.service';
 import { NativeEventService } from './services/native-event.service';
 import { MetaDataService } from './services/meta-data.service';
-import { PlayerConfig, VopStartCommand } from './models/verona';
-import { AlertDialogComponent } from './components/alert-dialog/alert-dialog.component';
 import { UnitStateService } from './services/unit-state.service';
 import { MediaPlayerService } from './services/media-player.service';
+import { AlertDialogComponent } from './components/alert-dialog/alert-dialog.component';
 import { Page } from '../../../common/models/page';
 
 @Component({
@@ -36,6 +35,7 @@ export class AppComponent implements OnInit {
               private nativeEventService: NativeEventService,
               private unitStateService: UnitStateService,
               private mediaPlayerService: MediaPlayerService,
+              private unitStateElementMapperService: UnitStateElementMapperService,
               private dialog: MatDialog) {
   }
 
@@ -61,6 +61,7 @@ export class AppComponent implements OnInit {
       console.log('player: onStart', message);
       if (message.unitDefinition) {
         const unitDefinition: Unit = new Unit(JSON.parse(message.unitDefinition));
+        this.unitStateElementMapperService.registerDropListValueIds(unitDefinition);
         if (this.metaDataService.verifyUnitDefinitionVersion(unitDefinition.veronaModuleVersion)) {
           this.playerConfig = message.playerConfig || {};
           this.veronaPostService.sessionId = message.sessionId;
@@ -99,5 +100,6 @@ export class AppComponent implements OnInit {
     this.playerConfig = {};
     this.unitStateService.reset();
     this.mediaPlayerService.reset();
+    this.unitStateElementMapperService.reset();
   }
 }
diff --git a/projects/player/src/app/components/element-container/element-container.component.ts b/projects/player/src/app/components/element-container/element-container.component.ts
index 7091c22b6..6c1192a88 100644
--- a/projects/player/src/app/components/element-container/element-container.component.ts
+++ b/projects/player/src/app/components/element-container/element-container.component.ts
@@ -6,30 +6,28 @@ import {
 } from '@angular/forms';
 import { takeUntil } from 'rxjs/operators';
 import { Subject } from 'rxjs';
-import { KeyboardService } from '../../services/keyboard.service';
-import { FormService } from '../../services/form.service';
-import { UnitStateService } from '../../services/unit-state.service';
-import { MarkingService } from '../../services/marking.service';
 import {
-  InputElement, InputElementValue, UIElement, ValueChangeElement
+  InputElement, UIElement, ValueChangeElement
 } from '../../../../../common/models/uI-element';
 import { FormElementComponent } from '../../../../../common/directives/form-element-component.directive';
 import { CompoundElementComponent }
   from '../../../../../common/directives/compound-element.directive';
-import { VideoElement } from '../../../../../common/ui-elements/video/video-element';
-import { AudioElement } from '../../../../../common/ui-elements/audio/audio-element';
-import { ImageElement } from '../../../../../common/ui-elements/image/image-element';
-import { VeronaPostService } from '../../services/verona-post.service';
 import { MediaPlayerElementComponent } from '../../../../../common/directives/media-player-element-component.directive';
-import { MediaPlayerService } from '../../services/media-player.service';
 import { TextComponent } from '../../../../../common/ui-elements/text/text.component';
 import { TextFieldElement } from '../../../../../common/ui-elements/text-field/text-field-element';
 import { ElementComponent } from '../../../../../common/directives/element-component.directive';
-import { ElementFactory } from '../../../../../common/util/element.factory';
 import { ImageComponent } from '../../../../../common/ui-elements/image/image.component';
 import { ButtonComponent } from '../../../../../common/ui-elements/button/button.component';
 import { TextFieldComponent } from '../../../../../common/ui-elements/text-field/text-field.component';
 import { TextAreaComponent } from '../../../../../common/ui-elements/text-area/text-area.component';
+import { ElementFactory } from '../../../../../common/util/element.factory';
+import { KeyboardService } from '../../services/keyboard.service';
+import { FormService } from '../../services/form.service';
+import { UnitStateService } from '../../services/unit-state.service';
+import { MarkingService } from '../../services/marking.service';
+import { MediaPlayerService } from '../../services/media-player.service';
+import { UnitStateElementMapperService } from '../../services/unit-state-element-mapper.service';
+import { VeronaPostService } from '../../services/verona-post.service';
 
 @Component({
   selector: 'app-element-container',
@@ -58,6 +56,7 @@ export class ElementContainerComponent implements OnInit {
               private formBuilder: FormBuilder,
               private veronaPostService: VeronaPostService,
               private mediaPlayerService: MediaPlayerService,
+              private unitStateElementMapperService: UnitStateElementMapperService,
               private markingService: MarkingService) {
   }
 
@@ -92,7 +91,12 @@ export class ElementContainerComponent implements OnInit {
     const elementComponentFactory =
       ElementFactory.getComponentFactory(this.elementModel.type, this.componentFactoryResolver);
     const elementComponent = this.elementComponentContainer.createComponent(elementComponentFactory).instance;
-    elementComponent.elementModel = this.restoreUnitStateValue(this.elementModel);
+    elementComponent.elementModel = this.unitStateElementMapperService
+      .mapToElementValue(
+        this.elementModel,
+        this.unitStateService.getUnitStateElement(this.elementModel.id),
+        this.markingService
+      );
     return elementComponent;
   }
 
@@ -126,7 +130,10 @@ export class ElementContainerComponent implements OnInit {
   private registerAtUnitStateService(elementComponent: ElementComponent): void {
     if (!(elementComponent instanceof CompoundElementComponent)) {
       this.unitStateService.registerElement(
-        this.initUnitStateValue(elementComponent.elementModel),
+        this.unitStateElementMapperService.mapToUnitStateValue(
+          elementComponent.elementModel,
+          this.unitStateService.getUnitStateElement(elementComponent.elementModel.id)
+        ),
         elementComponent.domElement,
         this.pageIndex
       );
@@ -142,9 +149,17 @@ export class ElementContainerComponent implements OnInit {
         .subscribe((children: QueryList<ElementComponent>) => {
           children.forEach((child, index) => {
             const childModel = compoundChildren[index];
-            child.elementModel = this.restoreUnitStateValue(childModel);
+            child.elementModel = this.unitStateElementMapperService
+              .mapToElementValue(
+                childModel,
+                this.unitStateService.getUnitStateElement(child.elementModel.id),
+                this.markingService
+              );
             this.unitStateService.registerElement(
-              this.initUnitStateValue(child.elementModel),
+              this.unitStateElementMapperService.mapToUnitStateValue(
+                child.elementModel,
+                this.unitStateService.getUnitStateElement(child.elementModel.id)
+              ),
               child.domElement,
               this.pageIndex
             );
@@ -171,7 +186,8 @@ export class ElementContainerComponent implements OnInit {
       elementComponent.applySelection
         .pipe(takeUntil(this.ngUnsubscribe))
         .subscribe((selection:
-        { mode: 'mark' | 'underline' | 'delete',
+        {
+          mode: 'mark' | 'underline' | 'delete',
           color: string;
           element: HTMLElement;
         }) => {
@@ -255,45 +271,6 @@ export class ElementContainerComponent implements OnInit {
     }
   }
 
-  private restoreUnitStateValue(elementModel: UIElement): UIElement {
-    const unitStateElementCode = this.unitStateService.getUnitStateElement(elementModel.id);
-    if (unitStateElementCode && unitStateElementCode.value !== undefined) {
-      switch (elementModel.type) {
-        case 'text':
-          elementModel.text = this.markingService
-            .restoreMarkings(unitStateElementCode.value as string[], this.elementModel.text);
-          break;
-        case 'image':
-          elementModel.magnifierUsed = unitStateElementCode.value;
-          break;
-        case 'video':
-        case 'audio':
-          if (elementModel && elementModel.playerProps) {
-            elementModel.playerProps.playbackTime = unitStateElementCode.value as number;
-          }
-          break;
-        default:
-          elementModel.value = unitStateElementCode.value;
-      }
-    }
-    return elementModel;
-  }
-
-  private initUnitStateValue = (elementModel: UIElement): { id: string, value: InputElementValue } => {
-    switch (elementModel.type) {
-      case 'text':
-        return { id: elementModel.id, value: [] };
-      case 'image':
-        return { id: elementModel.id, value: (elementModel as ImageElement).magnifierUsed };
-      case 'video':
-        return { id: elementModel.id, value: (elementModel as VideoElement).playerProps.playbackTime };
-      case 'audio':
-        return { id: elementModel.id, value: (elementModel as AudioElement).playerProps.playbackTime };
-      default:
-        return { id: elementModel.id, value: (elementModel as InputElement).value };
-    }
-  };
-
   private registerFormGroup(elementForm: FormGroup): void {
     this.formService.registerFormGroup({
       formGroup: elementForm,
diff --git a/projects/player/src/app/services/unit-state-element-mapper.service.ts b/projects/player/src/app/services/unit-state-element-mapper.service.ts
new file mode 100644
index 000000000..4f415a9e4
--- /dev/null
+++ b/projects/player/src/app/services/unit-state-element-mapper.service.ts
@@ -0,0 +1,91 @@
+import { Injectable } from '@angular/core';
+import { Unit } from '../../../../common/models/unit';
+import {
+  DragNDropValueObject, InputElement, InputElementValue, UIElement
+} from '../../../../common/models/uI-element';
+import { Section } from '../../../../common/models/section';
+import { ImageElement } from '../../../../common/ui-elements/image/image-element';
+import { VideoElement } from '../../../../common/ui-elements/video/video-element';
+import { AudioElement } from '../../../../common/ui-elements/audio/audio-element';
+import { DropListElement } from '../../../../common/ui-elements/drop-list/drop-list';
+import { UnitStateElementCode } from '../models/verona';
+import { MarkingService } from './marking.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UnitStateElementMapperService {
+  dropListValueIds!: DragNDropValueObject[];
+
+  registerDropListValueIds(unitDefinition: Unit): void {
+    this.dropListValueIds = unitDefinition.pages.reduce(
+      (accumulator: Section[], currentValue) => accumulator.concat(currentValue.sections), []
+    ).reduce(
+      (accumulator: UIElement[], currentValue) => accumulator.concat(currentValue.elements), []
+    ).filter(element => element.type === 'drop-list').reduce(
+      (accumulator: DragNDropValueObject[], currentValue: UIElement) => (
+        (currentValue.value && currentValue.value.length) ? accumulator.concat(currentValue.value) : accumulator), []
+    );
+  }
+
+  mapToElementValue(
+    elementModel: UIElement,
+    unitStateElement: UnitStateElementCode | undefined,
+    markingService: MarkingService
+  ): UIElement {
+    if (unitStateElement && unitStateElement.value !== undefined) {
+      switch (elementModel.type) {
+        case 'text':
+          elementModel.text = markingService
+            .restoreMarkings(unitStateElement.value as string[], elementModel.text);
+          break;
+        case 'image':
+          elementModel.magnifierUsed = unitStateElement.value;
+          break;
+        case 'video':
+        case 'audio':
+          if (elementModel && elementModel.playerProps) {
+            elementModel.playerProps.playbackTime = unitStateElement.value as number;
+          }
+          break;
+        case 'drop-list':
+          elementModel.value = (unitStateElement.value as string[])
+            .map(id => this.getDropListValueById(id));
+          break;
+        default:
+          elementModel.value = unitStateElement.value;
+      }
+    }
+    return elementModel;
+  }
+
+  mapToUnitStateValue = (elementModel: UIElement, unitStateElement: UnitStateElementCode | undefined):
+  { id: string, value: InputElementValue } => {
+    switch (elementModel.type) {
+      case 'text':
+        return { id: elementModel.id, value: unitStateElement?.value || [] };
+      case 'image':
+        return { id: elementModel.id, value: (elementModel as ImageElement).magnifierUsed };
+      case 'video':
+        return { id: elementModel.id, value: (elementModel as VideoElement).playerProps.playbackTime };
+      case 'audio':
+        return { id: elementModel.id, value: (elementModel as AudioElement).playerProps.playbackTime };
+      case 'drop-list':
+        return {
+          id: elementModel.id,
+          value: ((elementModel as DropListElement).value as DragNDropValueObject[])
+            .map(element => element.id)
+        };
+      default:
+        return { id: elementModel.id, value: (elementModel as InputElement).value };
+    }
+  };
+
+  reset(): void {
+    this.dropListValueIds = [];
+  }
+
+  private getDropListValueById(id: string): DragNDropValueObject | undefined {
+    return this.dropListValueIds.find(dropListValue => dropListValue.id === id);
+  }
+}
-- 
GitLab