From 683288f8d11f0ff22987569d0cfa926b15d65f82 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Wed, 3 Jul 2024 12:42:55 +0200
Subject: [PATCH] [editor] Refactor option adding and editing

Allow for direct adding of image options.
---
 .../likert-row-edit-dialog.component.ts       |  4 +-
 .../drop-list-properties.component.ts         |  8 +-
 .../options-field-set.component.ts            | 61 +++++++++---
 .../option-list-panel.component.ts            | 96 ++++++++++++++++---
 4 files changed, 138 insertions(+), 31 deletions(-)

diff --git a/projects/editor/src/app/components/dialogs/likert-row-edit-dialog.component.ts b/projects/editor/src/app/components/dialogs/likert-row-edit-dialog.component.ts
index f5da8d83e..9bfc77060 100644
--- a/projects/editor/src/app/components/dialogs/likert-row-edit-dialog.component.ts
+++ b/projects/editor/src/app/components/dialogs/likert-row-edit-dialog.component.ts
@@ -88,10 +88,10 @@ import { TextLabel } from 'common/models/elements/label-interfaces';
 export class LikertRowEditDialogComponent {
   constructor(@Inject(MAT_DIALOG_DATA) public data: { row: LikertRowElement, options: TextLabel[] }) { }
 
-  newLikertRow = {
+  newLikertRow = new LikertRowElement({
     ...this.data.row,
     rowLabel: { ...this.data.row.rowLabel }
-  };
+  });
 
   async loadImage(): Promise<void> {
     this.newLikertRow.rowLabel.imgSrc = await FileService.loadImage();
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts
index cdd1069c7..d8000ca1e 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts
@@ -55,10 +55,10 @@ export class GetValidDropListsPipe implements PipeTransform {
          class="fx-column-start-stretch">
       <aspect-option-list-panel [title]="'preset'" [textFieldLabel]="'Neue Option'"
                                 [itemList]="$any(combinedProperties.value)"
-                                (addItem)="addOption($event)"
-                                (removeItem)="removeOption($event)"
-                                (changedItemOrder)="moveOption('value', $event)"
-                                (editItem)="editOption($event)">
+                                (textItemAdded)="addOption($event)"
+                                (itemRemoved)="removeOption($event)"
+                                (itemReordered)="moveOption('value', $event)"
+                                (itemEdited)="editOption($event)">
       </aspect-option-list-panel>
 
       <mat-form-field *ngIf="combinedProperties.connectedTo !== null"
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts
index a0037ed7c..31920a2dc 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts
@@ -18,24 +18,28 @@ import { ElementService } from 'editor/src/app/services/unit-services/element.se
     <!--dropdown, radio-button-group-->
 <!--                              [useRichText]="combinedProperties.type === 'radio'"-->
     <aspect-option-list-panel *ngIf="combinedProperties.options !== undefined"
+                              [combinedProperties]="combinedProperties"
                               [title]="'propertiesPanel.options'"
                               [textFieldLabel]="'Neue Option'"
                               [itemList]="$any(combinedProperties.options)"
-                              (addItem)="addOption('options', $event)"
-                              (removeItem)="removeOption('options', $event)"
-                              (changedItemOrder)="moveOption('options', $event)"
-                              (editItem)="editOption('options', $event)">
+                              (textItemAdded)="addOption('options', $event)"
+                              (imageItemAdded)="addImageOption()"
+                              (itemRemoved)="removeOption('options', $event)"
+                              (itemReordered)="moveOption('options', $event)"
+                              (itemEdited)="editOption('options', $event)">
     </aspect-option-list-panel>
 
     <!--likert-->
     <aspect-option-list-panel *ngIf="combinedProperties.rows !== undefined"
+                              [combinedProperties]="combinedProperties"
                               [itemList]="$any(combinedProperties).rows | LikertRowLabel"
                               [title]="'rows'"
                               [textFieldLabel]="'Neue Zeile'"
-                              (changedItemOrder)="moveLikertRow($event)"
-                              (addItem)="addLikertRow($event)"
-                              (removeItem)="removeLikertRow($event)"
-                              (editItem)="editLikertRow($event)">
+                              (itemReordered)="moveLikertRow($event)"
+                              (textItemAdded)="addLikertRow($event)"
+                              (imageItemAdded)="addLikertRowImage()"
+                              (itemRemoved)="removeLikertRow($event)"
+                              (itemEdited)="editLikertRow($event)">
     </aspect-option-list-panel>
   `
 })
@@ -62,6 +66,20 @@ export class OptionsFieldSetComponent {
     });
   }
 
+  addImageOption() {
+    const selectedElements = this.selectionService.getSelectedElements() as OptionElement[];
+    const newLabel: Label = { text: '', imgSrc: null };
+    this.dialogService.showLabelEditDialog(newLabel)
+      .subscribe((result: Label) => {
+        if (result) {
+          selectedElements.forEach(element => {
+            const newValue = [...this.combinedProperties.options as Label[], result];
+            this.elementService.updateElementsProperty([element], 'options', newValue);
+          });
+        }
+      });
+  }
+
   removeOption(property: string, optionIndex: number): void {
     (this.combinedProperties[property] as Label[]).splice(optionIndex, 1);
     this.updateModel.emit({
@@ -77,9 +95,9 @@ export class OptionsFieldSetComponent {
     this.updateModel.emit({ property: property, value: this.combinedProperties[property] as Label[] });
   }
 
-  async editOption(property: string, optionIndex: number): Promise<void> {
+  editOption(property: string, optionIndex: number): void {
     const selectedOption = (this.combinedProperties[property] as Label[])[optionIndex];
-    await this.dialogService.showLabelEditDialog(selectedOption)
+    this.dialogService.showLabelEditDialog(selectedOption)
       .subscribe((result: Label) => {
         if (result) {
           (this.combinedProperties[property] as Label[])[optionIndex] = result;
@@ -106,11 +124,32 @@ export class OptionsFieldSetComponent {
     this.updateModel.emit({ property: 'rows', value: this.combinedProperties.rows as LikertRowElement[] });
   }
 
+  addLikertRowImage(): void {
+    const newRow = new LikertRowElement({
+      id: this.idService.getAndRegisterNewID('likert-row'),
+      rowLabel: {
+        text: '',
+        imgSrc: null,
+        imgPosition: 'above'
+      },
+      columnCount: (this.combinedProperties.options as unknown[]).length
+    } as LikertRowProperties);
+    const columns = this.combinedProperties.options as TextImageLabel[];
+
+    this.dialogService.showLikertRowEditDialog(newRow, columns)
+      .subscribe((result: LikertRowElement) => {
+        if (result) {
+          (this.combinedProperties.rows as LikertRowElement[]).push(result);
+          this.updateModel.emit({ property: 'rows', value: this.combinedProperties.rows as LikertRowElement[] });
+        }
+      });
+  }
+
   async editLikertRow(rowIndex: number): Promise<void> {
     const row = (this.combinedProperties.rows as LikertRowElement[])[rowIndex] as LikertRowElement;
     const columns = this.combinedProperties.options as TextImageLabel[];
 
-    await this.dialogService.showLikertRowEditDialog(row, columns)
+    this.dialogService.showLikertRowEditDialog(row, columns)
       .subscribe((result: LikertRowElement) => {
         if (result) {
           if (result.id !== row.id) {
diff --git a/projects/editor/src/app/components/properties-panel/option-list-panel.component.ts b/projects/editor/src/app/components/properties-panel/option-list-panel.component.ts
index fa4dcafc9..96af3414e 100644
--- a/projects/editor/src/app/components/properties-panel/option-list-panel.component.ts
+++ b/projects/editor/src/app/components/properties-panel/option-list-panel.component.ts
@@ -1,7 +1,7 @@
 import {
   Component, EventEmitter, Input, Output
 } from '@angular/core';
-import { CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
+import { CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
 import { Label } from 'common/models/elements/label-interfaces';
 import { TranslateModule } from '@ngx-translate/core';
 import { MatInputModule } from '@angular/material/input';
@@ -9,6 +9,8 @@ import { SharedModule } from 'common/shared.module';
 import { MatIconModule } from '@angular/material/icon';
 import { NgForOf, NgIf } from '@angular/common';
 import { MatButtonModule } from '@angular/material/button';
+import { DialogService } from 'editor/src/app/services/dialog.service';
+import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component';
 
 @Component({
   selector: 'aspect-option-list-panel',
@@ -24,16 +26,32 @@ import { MatButtonModule } from '@angular/material/button';
     SharedModule // TODO make pipe standalone and remove
   ],
   template: `
-    <fieldset class="fx-column-start-stretch">
-      <legend>{{title | translate }}</legend>
+    <fieldset *ngIf="title" class="fx-column-start-stretch">
+      <legend>{{ title | translate }}</legend>
+      <ng-container *ngTemplateOutlet="optionList"></ng-container>
+    </fieldset>
+
+    <div *ngIf="!title">
+      <ng-container *ngTemplateOutlet="optionList"></ng-container>
+    </div>
+
+    <ng-template #optionList>
       <mat-form-field appearance="outline">
-        <mat-label>{{textFieldLabel}}</mat-label>
+        <mat-label>{{ textFieldLabel }}</mat-label>
         <textarea #newItem matInput cdkTextareaAutosize type="text"
+                  (keydown.enter)="$event.stopPropagation(); $event.preventDefault();"
                   (keyup.enter)="addListItem(newItem.value); newItem.select()"></textarea>
         <button mat-icon-button matSuffix color="primary"
+                [disabled]="newItem.value === ''"
                 (click)="addListItem(newItem.value); newItem.select()">
           <mat-icon>add</mat-icon>
         </button>
+        <button *ngIf="combinedProperties.type !== 'dropdown' && combinedProperties.type !== 'radio'"
+                mat-icon-button matSuffix color="primary"
+                [matTooltip]="'Option mit Bild hinzufügen'"
+                (click)="addImageOption()">
+          <mat-icon>image</mat-icon>
+        </button>
       </mat-form-field>
 
       <div class="drop-list" cdkDropList [cdkDropListData]="itemList"
@@ -47,7 +65,7 @@ import { MatButtonModule } from '@angular/material/button';
           <img [src]="$any(item).imgSrc"
                [style.object-fit]="'scale-down'" [style.height.px]="40">
           <button mat-icon-button color="primary"
-                  (click)="editItem.emit(i)">
+                  (click)="editItem(i)">
             <mat-icon>build</mat-icon>
           </button>
           <button mat-icon-button color="primary"
@@ -56,7 +74,7 @@ import { MatButtonModule } from '@angular/material/button';
           </button>
         </div>
       </div>
-    </fieldset>
+    </ng-template>
   `,
   styles: [`
     .item-box {
@@ -76,23 +94,73 @@ import { MatButtonModule } from '@angular/material/button';
   `]
 })
 export class OptionListPanelComponent {
-  @Input() title!: string;
+  @Input() combinedProperties!: CombinedProperties;
+  @Input() title: string | undefined; // Fieldset is only rendered when given
   @Input() textFieldLabel!: string;
   @Input() itemList!: Label[];
-  @Output() addItem = new EventEmitter<string>();
-  @Output() removeItem = new EventEmitter<number>();
-  @Output() editItem = new EventEmitter<number>();
-  @Output() changedItemOrder = new EventEmitter<{ previousIndex: number, currentIndex: number }>();
+  @Input() localMode: boolean = false; // Edit list here instead of emitting events for everything
+  @Output() textItemAdded = new EventEmitter<string>();
+  @Output() imageItemAdded = new EventEmitter<void>();
+  @Output() itemRemoved = new EventEmitter<number>();
+  @Output() itemEdited = new EventEmitter<number>();
+  @Output() itemReordered = new EventEmitter<{ previousIndex: number, currentIndex: number }>();
+  @Output() itemListUpdated = new EventEmitter<void>();
+
+  constructor(private dialogService: DialogService) {}
 
   addListItem(text: string): void {
-    this.addItem.emit(text);
+    if (this.localMode) {
+      this.itemList.push({ text });
+      this.itemListUpdated.emit();
+    } else {
+      this.textItemAdded.emit(text);
+    }
+  }
+
+  addImageOption() {
+    if (this.localMode) {
+      const newLabel: Label = { text: '', imgSrc: null };
+      this.dialogService.showLabelEditDialog(newLabel)
+        .subscribe((result: Label) => {
+          if (result) {
+            this.itemList.push(result);
+            this.itemListUpdated.emit();
+          }
+        });
+    } else {
+      this.imageItemAdded.emit();
+    }
   }
 
   removeListItem(itemIndex: number): void {
-    this.removeItem.emit(itemIndex);
+    if (this.localMode) {
+      this.itemList.splice(itemIndex, 1);
+      this.itemListUpdated.emit();
+    } else {
+      this.itemRemoved.emit(itemIndex);
+    }
+  }
+
+  editItem(itemIndex: number): void {
+    if (this.localMode) {
+      this.dialogService.showLabelEditDialog(this.itemList[itemIndex])
+        .subscribe((result: Label) => {
+          if (result) {
+            this.itemList[itemIndex] = result;
+            this.itemListUpdated.emit();
+          }
+        });
+    } else {
+      this.itemEdited.emit(itemIndex);
+    }
   }
 
   moveListValue(event: CdkDragDrop<Label[]>): void {
-    this.changedItemOrder.emit({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });
+    if (this.localMode) {
+      moveItemInArray(this.itemList, event.previousIndex, event.currentIndex);
+      this.itemListUpdated.emit();
+    } else {
+      this.itemReordered.emit({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });
+    }
   }
 }
-- 
GitLab