From bda12d70dfecf6f2b419360f2f438bb05ef6de0d Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Tue, 30 Nov 2021 11:26:56 +0100
Subject: [PATCH] Refactor dnd values to be objects with IDs and add image
 support

- Dnd values now have a special type that has an ID, a string and
  another string for an image path.
- A new dialog exists for editing thoe options. Similarly to the likert
  header.
---
 projects/common/models/uI-element.ts          |  9 +++-
 .../drop-list/drop-list.component.ts          | 29 +++++++-----
 projects/editor/src/app/app.module.ts         |  4 +-
 .../drop-list-option-edit-dialog.component.ts | 44 +++++++++++++++++++
 .../element-model-properties.component.html   | 11 +++--
 .../element-model-properties.component.ts     | 11 +++++
 .../element-properties.component.ts           | 10 ++++-
 .../editor/src/app/services/dialog.service.ts | 17 ++++++-
 .../editor/src/app/services/unit.service.ts   | 19 +++++++-
 9 files changed, 131 insertions(+), 23 deletions(-)
 create mode 100644 projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts

diff --git a/projects/common/models/uI-element.ts b/projects/common/models/uI-element.ts
index 5c64b37e0..19042331a 100644
--- a/projects/common/models/uI-element.ts
+++ b/projects/common/models/uI-element.ts
@@ -4,7 +4,12 @@ import { IdService } from '../id.service';
 export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'checkbox'
 | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row' | 'radio-group-images'
 | 'drop-list' | 'cloze';
-export type InputElementValue = string[] | string | number | boolean | null;
+export type InputElementValue = string[] | string | number | boolean | DragNDropValueObject | null;
+export type DragNDropValueObject = {
+  id: string;
+  stringValue?: string;
+  imgSrcValue?: string;
+};
 
 export abstract class UIElement {
   [index: string]: any;
@@ -45,7 +50,7 @@ export abstract class UIElement {
 
   // This can be overwritten by elements if they need to handle some property specifics. Likert does.
   setProperty(property: string,
-              value: InputElementValue | string[] | LikertColumn[] | LikertRow[]): void {
+              value: InputElementValue | string[] | LikertColumn[] | LikertRow[] | DragNDropValueObject[]): void {
     if (this.fontProps && property in this.fontProps) {
       this.fontProps[property] = value as string | number | boolean;
     } else if (this.surfaceProps && property in this.surfaceProps) {
diff --git a/projects/common/ui-elements/drop-list/drop-list.component.ts b/projects/common/ui-elements/drop-list/drop-list.component.ts
index 9fe6c7fb9..dc37aa5dd 100644
--- a/projects/common/ui-elements/drop-list/drop-list.component.ts
+++ b/projects/common/ui-elements/drop-list/drop-list.component.ts
@@ -32,18 +32,25 @@ import { FormElementComponent } from '../../directives/form-element-component.di
            [cdkDropListOrientation]="elementModel.orientation"
            [cdkDropListEnterPredicate]="onlyOneItemPredicate"
            (cdkDropListDropped)="drop($event)">
-        <div class="item" *ngFor="let value of $any(elementModel.value)" cdkDrag
-             [style.background-color]="elementModel.itemBackgroundColor"
-             (cdkDragStarted)=dragStart() (cdkDragEnded)="dragEnd()">
-          <div *cdkDragPreview
-               [style.font-size.px]="elementModel.fontProps.fontSize"
-               [style.background-color]="elementModel.itemBackgroundColor">
-            {{value}}
+        <ng-container *ngFor="let value of $any(elementModel.value)">
+          <div class="item" *ngIf="!value.imgSrcValue" cdkDrag
+               [style.background-color]="elementModel.itemBackgroundColor"
+               (cdkDragStarted)=dragStart() (cdkDragEnded)="dragEnd()">
+            <div *cdkDragPreview
+                 [style.font-size.px]="elementModel.fontProps.fontSize"
+                 [style.background-color]="elementModel.itemBackgroundColor">
+              {{value.stringValue}}
+            </div>
+            <div class="drag-placeholder" *cdkDragPlaceholder [style.min-height.px]="elementModel.fontProps.fontSize">
+            </div>
+            {{value.stringValue}}
           </div>
-          <div class="drag-placeholder" *cdkDragPlaceholder [style.min-height.px]="elementModel.fontProps.fontSize">
-          </div>
-          {{value}}
-        </div>
+          <img *ngIf="value.imgSrcValue"
+               [src]="value.imgSrcValue | safeResourceUrl" alt="Image Placeholder"
+
+               cdkDrag (cdkDragStarted)=dragStart() (cdkDragEnded)="dragEnd()"
+               [style.object-fit]="'scale-down'">
+        </ng-container>
       </div>
       <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched"
                  class="error-message">
diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts
index 52be52c2e..7ea87272d 100644
--- a/projects/editor/src/app/app.module.ts
+++ b/projects/editor/src/app/app.module.ts
@@ -43,6 +43,7 @@ import { LikertColumnEditDialogComponent } from './components/dialogs/likert-col
 import { LikertRowEditDialogComponent } from './components/dialogs/likert-row-edit-dialog.component';
 import { RichTextEditDialogComponent } from './components/dialogs/rich-text-edit-dialog.component';
 import { ElementModelPropertiesComponent } from './components/unit-view/page-view/properties-panel/element-model-properties.component';
+import { DropListOptionEditDialogComponent } from './components/dialogs/drop-list-option-edit-dialog.component';
 
 @NgModule({
   declarations: [
@@ -68,7 +69,8 @@ import { ElementModelPropertiesComponent } from './components/unit-view/page-vie
     LikertColumnEditDialogComponent,
     LikertRowEditDialogComponent,
     RichTextEditDialogComponent,
-    ElementModelPropertiesComponent
+    ElementModelPropertiesComponent,
+    DropListOptionEditDialogComponent
   ],
   imports: [
     BrowserModule,
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
new file mode 100644
index 000000000..57f9aa9e0
--- /dev/null
+++ b/projects/editor/src/app/components/dialogs/drop-list-option-edit-dialog.component.ts
@@ -0,0 +1,44 @@
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { DragNDropValueObject } from '../../../../../common/models/uI-element';
+import { FileService } from '../../../../../common/file.service';
+
+@Component({
+  selector: 'app-drop-list-option-edit-dialog',
+  template: `
+    <mat-dialog-content fxLayout="column">
+      <mat-form-field>
+        <mat-label>{{'text' | translate }}</mat-label>
+        <input #textField matInput type="text" [value]="data.value.stringValue">
+      </mat-form-field>
+      <input #imageUpload type="file" hidden (click)="loadImage()">
+      <button mat-raised-button (click)="imageUpload.click()">{{ 'loadImage' | translate }}</button>
+      <button mat-raised-button (click)="imgSrc = undefined">{{ 'removeImage' | translate }}</button>
+      <img [src]="imgSrc"
+           [style.object-fit]="'scale-down'"
+           [width]="200">
+      <mat-form-field>
+        <mat-label>{{'id' | translate }}</mat-label>
+        <input #idField matInput type="text" [value]="data.value.id">
+      </mat-form-field>
+    </mat-dialog-content>
+    <mat-dialog-actions>
+      <button mat-button [mat-dialog-close]="{
+        stringValue: textField.value,
+        imgSrcValue: imgSrc,
+        id: idField.value
+      } ">
+        {{'save' | translate }}
+      </button>
+      <button mat-button mat-dialog-close>{{'cancel' | translate }}</button>
+    </mat-dialog-actions>
+  `
+})
+export class DropListOptionEditDialogComponent {
+  constructor(@Inject(MAT_DIALOG_DATA) public data: { value: DragNDropValueObject }) { }
+  imgSrc: string | undefined = this.data.value.imgSrcValue;
+
+  async loadImage(): Promise<void> {
+    this.imgSrc = await FileService.loadImage();
+  }
+}
diff --git a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.html b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.html
index 1037e5d9a..db17c834b 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.html
+++ b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.html
@@ -110,10 +110,13 @@
         <div *ngFor="let value of $any(combinedProperties.value); let i = index" cdkDrag
              class="list-items" fxLayout="row" fxLayoutAlign="end center">
           <div fxFlex="70">
-            {{value}}
+            {{value.stringValue}} ({{value.id}})
           </div>
+          <img [src]="value.imgSrcValue"
+               [style.object-fit]="'scale-down'"
+               [style.height.px]="40">
           <button mat-icon-button color="primary"
-                  (click)="editTextOption('value', i)">
+                  (click)="editDropListOption(i)">
             <mat-icon>build</mat-icon>
           </button>
           <button mat-icon-button color="primary"
@@ -125,11 +128,11 @@
     </ng-container>
     <div fxLayout="row" fxLayoutAlign="center center">
       <button mat-icon-button matPrefix
-              (click)="addOption('value', newValue.value); newValue.select()">
+              (click)="addDropListOption(newValue.value); newValue.select()">
         <mat-icon>add</mat-icon>
       </button>
       <input #newValue matInput type="text"
-             (keyup.enter)="addOption('value', newValue.value); newValue.select()">
+             (keyup.enter)="addDropListOption(newValue.value); newValue.select()">
     </div>
   </mat-form-field>
 
diff --git a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.ts
index 43d698e05..88faf36cd 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties.component.ts
@@ -5,6 +5,7 @@ import {
 import { CdkDragDrop } from '@angular/cdk/drag-drop/drag-events';
 import { moveItemInArray } from '@angular/cdk/drag-drop';
 import {
+  DragNDropValueObject,
   InputElementValue, LikertColumn, LikertRow, UIElement
 } from '../../../../../../../common/models/uI-element';
 import { LikertElement } from '../../../../../../../common/ui-elements/likert/likert-element';
@@ -50,6 +51,16 @@ export class ElementModelPropertiesComponent {
     await this.unitService.editTextOption(property, optionIndex);
   }
 
+  addDropListOption(value: string): void {
+    const id = this.unitService.getNewValueID();
+    this.combinedProperties.value.push({ stringValue: value, id: id });
+    this.updateModel.emit({ property: 'value', value: this.combinedProperties.value });
+  }
+
+  async editDropListOption(optionIndex: number): Promise<void> {
+    await this.unitService.editDropListOption(optionIndex);
+  }
+
   async editColumnOption(optionIndex: number): Promise<void> {
     await this.unitService.editLikertColumn(this.selectedElements as LikertElement[], optionIndex);
   }
diff --git a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-properties.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-properties.component.ts
index 41bcc0e6a..cf5469d8b 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-properties.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-properties.component.ts
@@ -8,7 +8,12 @@ import { TranslateService } from '@ngx-translate/core';
 import { UnitService } from '../../../../services/unit.service';
 import { SelectionService } from '../../../../services/selection.service';
 import { MessageService } from '../../../../../../../common/services/message.service';
-import { LikertColumn, LikertRow, UIElement } from '../../../../../../../common/models/uI-element';
+import {
+  DragNDropValueObject,
+  LikertColumn,
+  LikertRow,
+  UIElement
+} from '../../../../../../../common/models/uI-element';
 
 @Component({
   selector: 'app-element-properties',
@@ -73,7 +78,8 @@ export class ElementPropertiesComponent implements OnInit, OnDestroy {
   }
 
   updateModel(property: string,
-              value: string | number | boolean | string[] | LikertColumn[] | LikertRow[] | null,
+              value: string | number | boolean | string[] |
+              LikertColumn[] | LikertRow[] | DragNDropValueObject | null,
               isInputValid: boolean | null = true): void {
     if (isInputValid) {
       this.unitService.updateElementProperty(this.selectedElements, property, value);
diff --git a/projects/editor/src/app/services/dialog.service.ts b/projects/editor/src/app/services/dialog.service.ts
index 06b248aaf..36f46befe 100644
--- a/projects/editor/src/app/services/dialog.service.ts
+++ b/projects/editor/src/app/services/dialog.service.ts
@@ -9,7 +9,13 @@ import { RichTextEditDialogComponent } from '../components/dialogs/rich-text-edi
 import { PlayerEditDialogComponent } from '../components/dialogs/player-edit-dialog.component';
 import { LikertColumnEditDialogComponent } from '../components/dialogs/likert-column-edit-dialog.component';
 import { LikertRowEditDialogComponent } from '../components/dialogs/likert-row-edit-dialog.component';
-import { LikertColumn, PlayerElement, PlayerProperties } from '../../../../common/models/uI-element';
+import {
+  DragNDropValueObject,
+  LikertColumn,
+  PlayerElement,
+  PlayerProperties
+} from '../../../../common/models/uI-element';
+import { DropListOptionEditDialogComponent } from '../components/dialogs/drop-list-option-edit-dialog.component';
 
 @Injectable({
   providedIn: 'root'
@@ -35,6 +41,15 @@ export class DialogService {
     return dialogRef.afterClosed();
   }
 
+  showDropListOptionEditDialog(value: DragNDropValueObject): Observable<DragNDropValueObject> {
+    const dialogRef = this.dialog.open(DropListOptionEditDialogComponent, {
+      data: {
+        value: value
+      }
+    });
+    return dialogRef.afterClosed();
+  }
+
   showMultilineTextEditDialog(text: string): Observable<string> {
     const dialogRef = this.dialog.open(TextEditMultilineDialogComponent, {
       data: {
diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts
index a9325248c..a2dcff6d3 100644
--- a/projects/editor/src/app/services/unit.service.ts
+++ b/projects/editor/src/app/services/unit.service.ts
@@ -209,8 +209,8 @@ export class UnitService {
   }
 
   updateElementProperty(elements: UIElement[], property: string,
-                        value: string | number | boolean | string[] |
-                        LikertColumn[] | LikertRow[] | null): boolean {
+                        value: InputElementValue | LikertColumn[] | LikertRow[] |
+                        DragNDropValueObject[] | null): boolean {
     console.log('updateElementProperty', elements, property, value);
     for (const element of elements) {
       if (property === 'id') {
@@ -239,6 +239,21 @@ export class UnitService {
       });
   }
 
+  async editDropListOption(optionIndex: number): Promise<void> {
+    const oldOptions = this.selectionService.getSelectedElements()[0].value as DragNDropValueObject[];
+    await this.dialogService.showDropListOptionEditDialog(oldOptions[optionIndex])
+      .subscribe((result: DragNDropValueObject) => {
+        if (result) {
+          if (result.id !== oldOptions[optionIndex].id && !IdService.getInstance().isIdAvailable(result.id)) {
+            this.messageService.showError(this.translateService.instant('idTaken'));
+            return;
+          }
+          oldOptions[optionIndex] = result;
+          this.updateElementProperty(this.selectionService.getSelectedElements(), 'value', oldOptions);
+        }
+      });
+  }
+
   async editLikertRow(row: LikertElementRow, columns: LikertColumn[]): Promise<void> {
     await this.dialogService.showLikertRowEditDialog(row, columns)
       .subscribe((result: LikertElementRow) => {
-- 
GitLab