From 0ab8fe0ee60b4f3a0018221cb1b269ac59b60c2d Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Mon, 17 Jul 2023 12:54:47 +0200
Subject: [PATCH] Add audioSrc to DragNDropValueObject

---
 docs/unit_definition_changelog.txt            |  2 +
 .../input-elements/drop-list.component.ts     | 41 +++++++++++++++----
 .../models/elements/label-interfaces.ts       |  1 +
 .../common/services/sanitization.service.ts   |  2 +
 .../drop-list-option-edit-dialog.component.ts | 36 ++++++++++++----
 .../drop-list-properties.component.ts         |  3 +-
 projects/editor/src/assets/i18n/de.json       |  3 ++
 ...model-element-code-mapping.service.spec.ts | 14 ++++++-
 8 files changed, 84 insertions(+), 18 deletions(-)

diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt
index 691f25022..bebee0721 100644
--- a/docs/unit_definition_changelog.txt
+++ b/docs/unit_definition_changelog.txt
@@ -100,3 +100,5 @@ iqb-aspect-definition@1.0.0
   - add "stateVariables: StateVariable[]" (StateVariable: {id: string; value: string;})
 - UIElement:
   - add "isRelevantForPresentationComplete"
+- DragNDropValueObject:
+  - add "audioSrc"
diff --git a/projects/common/components/input-elements/drop-list.component.ts b/projects/common/components/input-elements/drop-list.component.ts
index ff03769b0..eb36fcbab 100644
--- a/projects/common/components/input-elements/drop-list.component.ts
+++ b/projects/common/components/input-elements/drop-list.component.ts
@@ -9,9 +9,8 @@ import {
   copyArrayItem
 } from '@angular/cdk/drag-drop';
 import { DropListElement } from 'common/models/elements/input-elements/drop-list';
-import { FormElementComponent } from '../../directives/form-element-component.directive';
-
 import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
+import { FormElementComponent } from '../../directives/form-element-component.directive';
 
 @Component({
   selector: 'aspect-drop-list',
@@ -54,13 +53,26 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
              class="list-item text-list-item"
              [class.hide-list-item]="parentForm && (elementModel.allowReplacement || elementModel.copyOnDrop) &&
                                      elementFormControl.value.length === 1 && showsPlaceholder"
+             [class.audio-list-item]="dropListValueElement.audioSrc"
              cdkDrag [cdkDragData]="dropListValueElement"
              (cdkDragStarted)="dragStart($event)"
              (cdkDragEnded)="dragEnd()"
              [style.background-color]="elementModel.styling.itemBackgroundColor">
-          <span>{{dropListValueElement.text}}</span>
+          <ng-container *ngIf="dropListValueElement.audioSrc">
+            <audio #player
+                   [src]="dropListValueElement.audioSrc | safeResourceUrl">
+            </audio>
+            <div class="audio-button"
+                 (click)="player.play()">
+              <mat-icon>play_arrow</mat-icon>
+            </div>
+          </ng-container>
+          <div class="text-padding">
+            {{dropListValueElement.text}}
+          </div>
           <ng-template cdkDragPreview matchSize>
             <div class="text-preview"
+                 [class.audio-list-item]="dropListValueElement.audioSrc"
                  [class.cloze-context-preview]="clozeContext"
                  [style.color]="elementModel.styling.fontColor"
                  [style.font-family]="elementModel.styling.font"
@@ -69,7 +81,11 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
                  [style.font-style]="elementModel.styling.italic ? 'italic' : ''"
                  [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''"
                  [style.background-color]="elementModel.styling.itemBackgroundColor">
-              <span>{{dropListValueElement.text}}</span>
+                <div *ngIf="dropListValueElement.audioSrc"
+                     class="audio-button">
+                  <mat-icon>play_arrow</mat-icon>
+                </div>
+              <div class="text-padding">{{dropListValueElement.text}}</div>
             </div>
           </ng-template>
         </div>
@@ -96,7 +112,7 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
                                     (!elementFormControl.value.length && !showsPlaceholder) ||
                                     (elementFormControl.value.length === 1 && !showsPlaceholder && dragging) :
                                     !elementModel.value.length;">
-        <span class="baseline-helper">&nbsp;</span>
+        <div class="baseline-helper text-padding">&nbsp;</div>
         <mat-error *ngIf="elementFormControl.errors &&
                           elementFormControl.touched &&
                           !classReference.highlightReceivingDropList"
@@ -105,6 +121,11 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
     </div>
   `,
   styles: [
+    '.audio-button {cursor: pointer;}',
+    '.audio-button:hover {color: #006064;}',
+    '.cloze-context .list-item.text-list-item .audio-button .mat-icon {height: 19px;}',
+    ':not(.cloze-context) .list-item.text-list-item .audio-button .mat-icon {margin-top: 5px; padding-left: 3px;}',
+    '.audio-list-item {display: flex; flex-direction: row; align-items: center; justify-content: flex-start;}',
     '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); border-radius: 5px;}',
     '.list {display: flex; box-sizing: border-box; padding: 5px;}',
     '.list.vertical-orientation {flex-direction: column;}',
@@ -116,13 +137,17 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
     '.error {padding: 3px; border: 2px solid #f44336 !important;}',
     '.list-item:active {cursor: grabbing;}',
     '.list-item {border-radius: 5px;}',
-    ':not(.cloze-context) .list-item.text-list-item {padding: 10px;}',
-    '.cloze-context .list-item.text-list-item {padding: 0 5px;}',
+    ':not(.cloze-context) .list-item.text-list-item:not(.audio-list-item) .text-padding {padding: 10px;}',
+    ':not(.cloze-context) .list-item.text-list-item.audio-list-item .text-padding {padding: 10px 10px 10px 5px;}',
+    '.cloze-context .list-item.text-list-item .text-padding {padding: 0 5px;}',
     '.cloze-context.only-one-item .list-item {height: 100%; display: flex; align-items: center; justify-content: center;}',
     '.image-list-item {align-self: flex-start;}',
     '.hide-list-item {display: none !important; transform: unset !important;}',
     '.cdk-drag-preview {border-radius: 5px; box-shadow: 2px 2px 5px black;}',
-    '.cdk-drag-preview.text-preview {padding: 10px; box-sizing: border-box;}',
+    '.cdk-drag-preview.text-preview {box-sizing: border-box;}',
+    '.cdk-drag-preview.text-preview:not(.audio-list-item) .text-padding{padding: 10px;}',
+    '.cdk-drag-preview.text-preview.audio-list-item .text-padding{padding: 10px 10px 10px 5px;}',
+    '.cdk-drag-preview.text-preview .audio-button .mat-icon {margin-top: 5px; padding-left: 3px;}',
     '.cdk-drag-preview.cloze-context-preview {padding: 0 2px; display: flex; align-items: center; justify-content: center;}',
     '.cdk-drop-list-dragging .cdk-drag {transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}',
     '.cdk-drag-placeholder {background-color: #ccc !important;}',
diff --git a/projects/common/models/elements/label-interfaces.ts b/projects/common/models/elements/label-interfaces.ts
index 0cd9c256c..5d9560dca 100644
--- a/projects/common/models/elements/label-interfaces.ts
+++ b/projects/common/models/elements/label-interfaces.ts
@@ -11,6 +11,7 @@ export interface DragNDropValueObject extends TextImageLabel {
   id: string;
   originListID: string;
   originListIndex: number;
+  audioSrc: string | null;
 }
 
 export type Label = TextLabel | TextImageLabel | DragNDropValueObject;
diff --git a/projects/common/services/sanitization.service.ts b/projects/common/services/sanitization.service.ts
index 52807533f..d7948fd55 100644
--- a/projects/common/services/sanitization.service.ts
+++ b/projects/common/services/sanitization.service.ts
@@ -381,6 +381,7 @@ export class SanitizationService {
           id: 'id_placeholder',
           text: option,
           imgSrc: null,
+          audioSrc: null,
           imgPosition: 'above',
           originListID: newElement.id as string,
           originListIndex: index
@@ -396,6 +397,7 @@ export class SanitizationService {
           id: 'id_placeholder',
           text: value,
           imgSrc: null,
+          audioSrc: null,
           imgPosition: 'above',
           originListID: newElement.id as string,
           originListIndex: index
diff --git a/projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts b/projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts
index ac62db753..8efd7c3cb 100644
--- a/projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts
+++ b/projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts
@@ -1,23 +1,31 @@
 import { Component, Inject } from '@angular/core';
 import { MAT_DIALOG_DATA } from '@angular/material/dialog';
 import { FileService } from 'common/services/file.service';
-
-
 import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
 
 @Component({
   selector: 'aspect-drop-list-option-edit-dialog',
   template: `
-    <mat-dialog-content class="fx-column-start-stretch">
+    <mat-dialog-content class="fx-column-start-stretch fx-gap-20">
       <mat-form-field>
         <mat-label>{{'text' | translate }}</mat-label>
         <input #textField matInput type="text" [value]="data.value.text">
       </mat-form-field>
-      <button mat-raised-button (click)="loadImage()">{{ 'loadImage' | translate }}</button>
-      <button mat-raised-button (click)="imgSrc = null">{{ 'removeImage' | translate }}</button>
-      <img [src]="imgSrc"
-           [style.object-fit]="'scale-down'"
-           [width]="200">
+      <div *ngIf="!audioSrc" class="fx-column-start-stretch fx-gap-3">
+        <button mat-raised-button (click)="loadImage()">{{ 'loadImage' | translate }}</button>
+        <button mat-raised-button (click)="imgSrc = null">{{ 'removeImage' | translate }}</button>
+        <img [src]="imgSrc"
+             [style.object-fit]="'scale-down'"
+             [width]="200">
+      </div>
+      <div *ngIf="!imgSrc" class="fx-column-start-stretch fx-gap-3">
+        <button mat-raised-button (click)="loadAudio()">
+          {{(audioSrc ? 'changeAudio' : 'loadAudio') | translate }}
+        </button>
+        <button mat-raised-button (click)="audioSrc = null">
+          {{ 'removeAudio' | translate }}
+        </button>
+      </div>
       <mat-form-field>
         <mat-label>{{'id' | translate }}</mat-label>
         <input #idField matInput type="text" [value]="data.value.id">
@@ -27,6 +35,7 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
       <button mat-button [mat-dialog-close]="{
         text: textField.value,
         imgSrc: imgSrc,
+        audioSrc: audioSrc,
         id: idField.value
       }">
         {{'save' | translate }}
@@ -35,6 +44,12 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
     </mat-dialog-actions>
   `,
   styles: [`
+    .fx-gap-3 {
+      gap: 3px;
+    }
+    .fx-gap-20 {
+      gap: 20px;
+    }
     .fx-column-start-stretch {
       box-sizing: border-box;
       display: flex;
@@ -47,8 +62,13 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
 export class DropListOptionEditDialogComponent {
   constructor(@Inject(MAT_DIALOG_DATA) public data: { value: DragNDropValueObject }) { }
   imgSrc: string | null = this.data.value.imgSrc;
+  audioSrc: string | null = this.data.value.audioSrc;
 
   async loadImage(): Promise<void> {
     this.imgSrc = await FileService.loadImage();
   }
+
+  async loadAudio() {
+    this.audioSrc = await FileService.loadAudio();
+  }
 }
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts
index 0812db203..88c17113b 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts
@@ -7,11 +7,11 @@ import { moveItemInArray } from '@angular/cdk/drag-drop';
 import { MessageService } from 'common/services/message.service';
 import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component';
 import { IDService } from 'editor/src/app/services/id.service';
+import { DragNDropValueObject, TextImageLabel } from 'common/models/elements/label-interfaces';
 import { UnitService } from '../../../../services/unit.service';
 import { SelectionService } from '../../../../services/selection.service';
 import { DialogService } from '../../../../services/dialog.service';
 
-import { DragNDropValueObject, TextImageLabel } from 'common/models/elements/label-interfaces';
 
 @Component({
   selector: 'aspect-drop-list-properties',
@@ -141,6 +141,7 @@ export class DropListPropertiesComponent {
         {
           text: value,
           imgSrc: null,
+          audioSrc: null,
           imgPosition: 'above',
           id: this.unitService.getNewValueID(),
           originListID: 'id_placeholder',
diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json
index 7c3b9c21d..9342ed51e 100644
--- a/projects/editor/src/assets/i18n/de.json
+++ b/projects/editor/src/assets/i18n/de.json
@@ -22,6 +22,9 @@
   "text": "Text",
   "loadImage": "Bild laden",
   "removeImage": "Bild entfernen",
+  "loadAudio": "Audio laden",
+  "changeAudio": "Audio ändern",
+  "removeAudio": "Audio entfernen",
   "above": "oberhalb",
   "below": "unterhalb",
   "preset": "Vorbelegung",
diff --git a/projects/player/src/app/services/element-model-element-code-mapping.service.spec.ts b/projects/player/src/app/services/element-model-element-code-mapping.service.spec.ts
index 168a3cafc..569f39cb2 100644
--- a/projects/player/src/app/services/element-model-element-code-mapping.service.spec.ts
+++ b/projects/player/src/app/services/element-model-element-code-mapping.service.spec.ts
@@ -29,8 +29,8 @@ import { RadioButtonGroupComplexElement } from 'common/models/elements/input-ele
 import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row';
 import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button';
 import { Hotspot, HotspotImageElement } from 'common/models/elements/input-elements/hotspot-image';
-import { ElementModelElementCodeMappingService } from './element-model-element-code-mapping.service';
 import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
+import { ElementModelElementCodeMappingService } from './element-model-element-code-mapping.service';
 
 describe('ElementModelElementCodeMappingService', () => {
   let service: ElementModelElementCodeMappingService;
@@ -235,6 +235,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'a',
         id: 'value_1',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -243,6 +244,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'b',
         id: 'value_2',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -251,6 +253,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'c',
         id: 'value_3',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -259,6 +262,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'd',
         id: 'value_4',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -267,6 +271,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'e',
         id: 'value_5',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -278,6 +283,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'e',
         id: 'value_5',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -293,6 +299,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'a',
         id: 'value_1',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 0
@@ -301,6 +308,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'b',
         id: 'value_2',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 1
@@ -309,6 +317,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'c',
         id: 'value_3',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 2
@@ -317,6 +326,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'd',
         id: 'value_4',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 3
@@ -325,6 +335,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'e',
         id: 'value_5',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 4
@@ -336,6 +347,7 @@ describe('ElementModelElementCodeMappingService', () => {
         text: 'e',
         id: 'value_5',
         imgSrc: null,
+        audioSrc: null,
         imgPosition: 'above',
         originListID: 'id',
         originListIndex: 4
-- 
GitLab