From 39882c75c0b2c2e643ac107a83499ef1b9013ae7 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Thu, 1 Dec 2022 00:52:52 +0100
Subject: [PATCH] Fix and refactor DropList component

Rules for when drop is allowed have been fixed. It is quite complicated
and therefore a lot of explanatory comments are added.

- improved translation
- add example file
---
 example_data/droplist/sortierlisten.json      |  1 +
 .../input-elements/drop-list.component.ts     | 82 +++++++++++++------
 projects/editor/src/assets/i18n/de.json       |  2 +-
 3 files changed, 60 insertions(+), 25 deletions(-)
 create mode 100644 example_data/droplist/sortierlisten.json

diff --git a/example_data/droplist/sortierlisten.json b/example_data/droplist/sortierlisten.json
new file mode 100644
index 000000000..ab5acbc40
--- /dev/null
+++ b/example_data/droplist/sortierlisten.json
@@ -0,0 +1 @@
+{"type":"aspect-unit-definition","pages":[{"sections":[{"elements":[{"width":180,"height":100,"type":"drop-list","id":"drop-list_1","label":"Beschriftung","value":[{"text":"aaa","imgSrc":null,"imgPosition":"above","id":"value_1","returnToOriginOnReplacement":false},{"text":"bbb","imgSrc":null,"imgPosition":"above","id":"value_2","returnToOriginOnReplacement":false},{"text":"allow replace","imgSrc":null,"id":"value_3","returnToOriginOnReplacement":true,"originListID":"drop-list_1","originListIndex":2}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":true,"connectedTo":["drop-list_2"],"copyOnDrop":false,"deleteDroppedItemWithSameID":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":60,"type":"button","id":"button_1","label":"only one","imageSrc":null,"asLink":false,"action":null,"actionParam":null,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"borderRadius":0,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false,"backgroundColor":"#d3d3d3"}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_2","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":true,"isSortList":true,"connectedTo":["drop-list_1","drop-list_3"],"copyOnDrop":false,"deleteDroppedItemWithSameID":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null,"activeAfterIdDelay":0},{"elements":[{"width":180,"height":60,"type":"button","id":"button_2","label":"Navigationsknopf","imageSrc":null,"asLink":false,"action":null,"actionParam":null,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"borderRadius":0,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false,"backgroundColor":"#d3d3d3"}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_3","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":true,"connectedTo":["drop-list_1","drop-list_2"],"copyOnDrop":false,"deleteDroppedItemWithSameID":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null,"activeAfterIdDelay":0}],"hasMaxWidth":false,"maxWidth":900,"margin":30,"backgroundColor":"#ffffff","alwaysVisible":false,"alwaysVisiblePagePosition":"left","alwaysVisibleAspectRatio":50}],"version":"3.8.0"}
\ No newline at end of file
diff --git a/projects/common/components/input-elements/drop-list.component.ts b/projects/common/components/input-elements/drop-list.component.ts
index 344b19229..1a3fa9971 100644
--- a/projects/common/components/input-elements/drop-list.component.ts
+++ b/projects/common/components/input-elements/drop-list.component.ts
@@ -37,7 +37,7 @@ import { FormElementComponent } from '../../directives/form-element-component.di
              class="list-item"
              draggable="true"
              (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)"
-             (dragenter)="moveElementInSortList($event)"
+             (dragenter)="dragEnterItem($event)"
              [class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index"
              [class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index"
              [style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''"
@@ -50,7 +50,7 @@ import { FormElementComponent } from '../../directives/form-element-component.di
              [id]="dropListValueElement.id"
              draggable="true"
              (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)"
-             (dragenter)="moveElementInSortList($event)"
+             (dragenter)="dragEnterItem($event)"
              [class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index"
              [class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index"
              [style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''">
@@ -163,17 +163,22 @@ export class DropListComponent extends FormElementComponent implements OnInit, A
     return dragImage;
   }
 
-  moveElementInSortList(event: DragEvent) {
+  dragEnterItem(event: DragEvent) {
     event.preventDefault();
     if (this.elementModel.isSortList && DropListComponent.sourceList === this) {
-      const sourceIndex: number = this.placeHolderIndex as number;
-      const targetIndex: number = Array.from((event.target as any).parentNode.children).indexOf(event.target);
-      const removedElement = this.viewModel.splice(sourceIndex, 1)[0];
-      this.viewModel.splice(targetIndex, 0, removedElement);
-      this.placeHolderIndex = targetIndex;
+      this.moveListItem(
+        this.placeHolderIndex as number,
+        Array.from((event.target as any).parentNode.children).indexOf(event.target)
+      );
     }
   }
 
+  moveListItem(sourceIndex: number, targetIndex: number): void {
+    const removedElement = this.viewModel.splice(sourceIndex, 1)[0];
+    this.viewModel.splice(targetIndex, 0, removedElement);
+    this.placeHolderIndex = targetIndex;
+  }
+
   dragEnterList(event: DragEvent) {
     event.preventDefault();
 
@@ -184,8 +189,7 @@ export class DropListComponent extends FormElementComponent implements OnInit, A
     } else if (DropListComponent.sourceList !== this) {
       this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject);
       const sourceList = DropListComponent.sourceList as DropListComponent;
-      sourceList.viewModel.splice(sourceList.placeHolderIndex as number, 1);
-      sourceList.elementFormControl.setValue(sourceList.viewModel);
+      DropListComponent.removeElementFromList(sourceList, sourceList.placeHolderIndex as number);
       sourceList.placeHolderIndex = undefined;
       DropListComponent.sourceList = this;
       this.placeHolderIndex = this.viewModel.length > 0 ? this.viewModel.length - 1 : 0;
@@ -197,30 +201,31 @@ export class DropListComponent extends FormElementComponent implements OnInit, A
     this.highlightValidDrop = false;
   }
 
-  drop(event: DragEvent) {
+  drop(event: DragEvent): void {
     event.preventDefault();
 
     // SortList viewModel already gets manipulated while dragging. Just set the value.
     if (DropListComponent.sourceList === this && this.elementModel.isSortList) {
       this.elementFormControl.setValue(this.viewModel);
-    // if drop is allowed that means item transfer
-    } else if (this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) {
-      const valueIDs = this.elementFormControl.value.map((valueValue: DragNDropValueObject) => valueValue.id);
-      const isIDAlreadyPresent = valueIDs.includes(DropListComponent.draggedElement?.id);
-      if (!isIDAlreadyPresent) {
+      this.dragEnd();
+      return;
+    }
+    // if drop is allowed that means item transfer between non-sort lists
+    if (this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) {
+      if (!this.isIDAlreadyPresent()) {
         if (this.elementModel.onlyOneItem &&
             this.viewModel.length > 0 &&
-            this.viewModel[0].returnToOriginOnReplacement) {
+            this.viewModel[0].returnToOriginOnReplacement) { // move replaced item back to origin
           const originListComponent = DropListComponent.dragAndDropComponents[this.viewModel[0].originListID as string];
           DropListComponent.addElementToList(originListComponent, this.viewModel[0]);
           DropListComponent.removeElementFromList(this, 0);
         }
-        DropListComponent.addElementToList(this, DropListComponent.draggedElement as DragNDropValueObject);
-        if (!DropListComponent.sourceList?.elementModel.copyOnDrop) {
+        if (!DropListComponent.sourceList?.elementModel.copyOnDrop) { // remove source item if not copy
           DropListComponent.removeElementFromList(DropListComponent.sourceList as DropListComponent,
             DropListComponent.sourceList?.placeHolderIndex as number);
         }
-      } else if (this.elementModel.deleteDroppedItemWithSameID) {
+        DropListComponent.addElementToList(this, DropListComponent.draggedElement as DragNDropValueObject);
+      } else if (this.elementModel.deleteDroppedItemWithSameID) { // put back (return) item
         DropListComponent.removeElementFromList(DropListComponent.sourceList as DropListComponent,
           DropListComponent.sourceList?.placeHolderIndex as number);
       }
@@ -228,10 +233,39 @@ export class DropListComponent extends FormElementComponent implements OnInit, A
     this.dragEnd();
   }
 
+  /* When
+  - same list
+  - connected list
+  - onlyOneItem && itemcount = 0 ||
+  onlyOneItem && itemcount = 1 && this.viewModel[0].returnToOriginOnReplacement)  // verdraengen
+  - (! id already present) || id already present && deleteDroppedItemWithSameID // zuruecklegen
+   */
   isDropAllowed(connectedDropLists: string[]): boolean {
-    return (DropListComponent.sourceList === this) ||
-      ((connectedDropLists as string[]).includes(this.elementModel.id) &&
-       !(this.elementModel.onlyOneItem && this.viewModel.length > 0 && !this.viewModel[0].returnToOriginOnReplacement));
+    const sameList = DropListComponent.sourceList === this;
+    const isConnectedList = (connectedDropLists as string[]).includes(this.elementModel.id);
+    return (sameList) || (isConnectedList &&
+                             !this.isOnlyOneItemAndNoReplacingOrReturning() &&
+                             !this.isIDPresentAndNoReturning());
+  }
+
+  isIDPresentAndNoReturning(): boolean {
+    return this.isIDAlreadyPresent() && !(this.elementModel.deleteDroppedItemWithSameID);
+  }
+
+  /* No replacement in sort lists: operation should only move the placeholder
+     until the actual drop. By allowing elements to transfer while dragging we create
+     all kinds of problems and unwanted behaviour, like having all touched lists generate change events.
+   */
+  isOnlyOneItemAndNoReplacingOrReturning(): boolean {
+    return this.elementModel.onlyOneItem && this.viewModel.length > 0 &&
+      !((this.viewModel[0].returnToOriginOnReplacement && !this.elementModel.isSortList) ||
+       (this.elementModel.deleteDroppedItemWithSameID &&
+         DropListComponent.draggedElement?.id === this.viewModel[0].id));
+  }
+
+  isIDAlreadyPresent(): boolean {
+    const listValueIDs = this.elementFormControl.value.map((valueValue: DragNDropValueObject) => valueValue.id);
+    return listValueIDs.includes(DropListComponent.draggedElement?.id);
   }
 
   static addElementToList(listComponent: DropListComponent, element: DragNDropValueObject, targetIndex?: number): void {
@@ -252,7 +286,7 @@ export class DropListComponent extends FormElementComponent implements OnInit, A
     listComponent.elementFormControl.setValue(listComponent.viewModel);
   }
 
-  dragEnd(event?: DragEvent) {
+  dragEnd(event?: DragEvent): void {
     event?.preventDefault();
 
     Object.entries(DropListComponent.dragAndDropComponents)
diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json
index 40897b882..dc2716a01 100644
--- a/projects/editor/src/assets/i18n/de.json
+++ b/projects/editor/src/assets/i18n/de.json
@@ -190,7 +190,7 @@
     "hasDynamicRowCount": "Dynamische Zeilen",
     "expectedCharactersCount": "Erwartete Zeichenanzahl",
     "isSortList": "Sortierliste",
-    "deleteDroppedItemWithSameID": "Gleiche abgelegte Elemente löschen",
+    "deleteDroppedItemWithSameID": "Zurücklegen erlauben",
     "deleteDroppedItemWithSameIDTooltip": "Elemente mit gleicher ID werden beim Zurücklegen gelöscht.",
     "setElementInteractionEnabled": "Elementinteraktion erlauben",
     "enableModeSwitch": "Eingabemodus änderbar"
-- 
GitLab