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