diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index b3d50bd9eec4c0a415fe6a0e973611e7f284718d..233a91dda7f482d395a6f392b59fe4c278088a0c 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -62,3 +62,7 @@ iqb-aspect-definition@1.0.0 3.9.0 - DropListElement: +deleteDroppedItemWithSameID +- DropListValueElement: + + returnToOriginOnReplacement?: boolean; + + originListID?: string; + + originListIndex?: number; diff --git a/projects/common/components/input-elements/drop-list.component.ts b/projects/common/components/input-elements/drop-list.component.ts index 4c16df4a76d0ffd69b268a898f0eef3029b4473e..344b1922937f6061f19ed3c81189651dc0334a0b 100644 --- a/projects/common/components/input-elements/drop-list.component.ts +++ b/projects/common/components/input-elements/drop-list.component.ts @@ -10,60 +10,62 @@ import { FormElementComponent } from '../../directives/form-element-component.di @Component({ selector: 'aspect-drop-list', template: ` - <div class="list" [id]="elementModel.id" - [fxLayout]="elementModel.orientation | droplistLayout" - [fxLayoutAlign]="elementModel.orientation | droplistLayoutAlign" - [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', + <div class="list" [id]="elementModel.id" + [fxLayout]="elementModel.orientation | droplistLayout" + [fxLayoutAlign]="elementModel.orientation | droplistLayoutAlign" + [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', 'horizontal-orientation' : elementModel.orientation === 'horizontal', 'clozeContext': clozeContext}" - [style.min-height.px]="elementModel.position?.useMinHeight ? elementModel.height : undefined" - [style.color]="elementModel.styling.fontColor" - [style.font-family]="elementModel.styling.font" - [style.font-size.px]="elementModel.styling.fontSize" - [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" - [style.font-style]="elementModel.styling.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" - [style.backgroundColor]="elementModel.styling.backgroundColor" - [class.errors]="elementFormControl.errors && elementFormControl.touched" - [style.outline-color]="elementModel.highlightReceivingDropListColor" - [class.highlight-valid-drop]="highlightValidDrop" - [class.highlight-as-receiver]="highlightAsReceiver" - tabindex="0" - (focusout)="elementFormControl.markAsTouched()" - (drop)="drop($event)" (dragenter)="dragEnterList($event)" (dragleave)="dragLeaveList($event)" - (dragover)="$event.preventDefault()"> - <ng-container *ngFor="let dropListValueElement of viewModel let index = index;"> - <div *ngIf="!dropListValueElement.imgSrc" - class="list-item" - draggable="true" - (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($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' : ''" - [style.background-color]="elementModel.styling.itemBackgroundColor"> - <span>{{dropListValueElement.text}}</span> - </div> - <img *ngIf="dropListValueElement.imgSrc" - class="list-item" - [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder" - [id]="dropListValueElement.id" - draggable="true" - (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($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' : ''"> - </ng-container> - </div> - <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" - class="error-message"> - {{elementFormControl.errors | errorTransform: elementModel}} - </mat-error> + [style.min-height.px]="elementModel.position?.useMinHeight ? elementModel.height : undefined" + [style.color]="elementModel.styling.fontColor" + [style.font-family]="elementModel.styling.font" + [style.font-size.px]="elementModel.styling.fontSize" + [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" + [style.font-style]="elementModel.styling.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" + [style.backgroundColor]="elementModel.styling.backgroundColor" + [class.errors]="elementFormControl.errors && elementFormControl.touched" + [style.outline-color]="elementModel.highlightReceivingDropListColor" + [class.highlight-valid-drop]="highlightValidDrop" + [class.highlight-as-receiver]="highlightAsReceiver" + tabindex="0" + (focusout)="elementFormControl.markAsTouched()" + (drop)="drop($event)" (dragenter)="dragEnterList($event)" (dragleave)="dragLeaveList($event)" + (dragover)="$event.preventDefault()"> + <ng-container *ngFor="let dropListValueElement of viewModel let index = index;"> + <div *ngIf="!dropListValueElement.imgSrc" + class="list-item" + draggable="true" + (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)" + (dragenter)="moveElementInSortList($event)" + [class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index" + [class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index" + [style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''" + [style.background-color]="elementModel.styling.itemBackgroundColor"> + <span>{{dropListValueElement.text}}</span> + </div> + <img *ngIf="dropListValueElement.imgSrc" + class="list-item" + [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder" + [id]="dropListValueElement.id" + draggable="true" + (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)" + (dragenter)="moveElementInSortList($event)" + [class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index" + [class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index" + [style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''"> + </ng-container> + </div> + <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" + class="error-message"> + {{elementFormControl.errors | errorTransform: elementModel}} + </mat-error> `, styles: [ - '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); padding: 3px;}', + '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); border-radius: 5px;}', + ':not(.clozeContext).list {padding: 3px;}', ':not(.clozeContext) .list-item {border-radius: 5px; padding: 10px;}', + '.clozeContext .list-item {border-radius: 5px; padding: 0 5px; text-align: center;}', 'img.list-item {align-self: start;}', '.vertical-orientation .list-item:not(:last-child) {margin-bottom: 5px;}', '.horizontal-orientation .list-item:not(:last-child) {margin-right: 5px;}', @@ -107,6 +109,7 @@ export class DropListComponent extends FormElementComponent implements OnInit, A } // TODO method names + // TODO elemente flackern manchmal beim aufnehmen; iwas stimmt mit highlightAsReceiver nicht dragStart(dragEvent: DragEvent, dropListValueElement: DragNDropValueObject, sourceListIndex: number) { @@ -116,7 +119,7 @@ export class DropListComponent extends FormElementComponent implements OnInit, A this.createDragImage(dragEvent.target as HTMLElement, dropListValueElement.id), 0, 0); } - // Sadly timeout is necessary for Chrome, which does not allow DOM manipulation on dragstart + // Timeout is necessary for Chrome, which does not allow DOM manipulation on dragstart setTimeout(() => { DropListComponent.draggedElement = dropListValueElement; DropListComponent.sourceList = this; @@ -128,25 +131,31 @@ export class DropListComponent extends FormElementComponent implements OnInit, A this.highlightValidDrop = true; } + /* Let all droplists know when drag is going on, so they can potentially disable their pointer effects. + * This is to prevent unwanted dragOver events of list items. */ Object.entries(DropListComponent.dragAndDropComponents) .forEach(([, value]) => { value.dragging = true; }); if (this.elementModel.highlightReceivingDropList) { - this.highlightAsReceiver = true; - this.elementModel.connectedTo.forEach(connectedDropListID => { - DropListComponent.dragAndDropComponents[connectedDropListID].highlightAsReceiver = true; - }); + this.highlightReceiverLists(); } }); } + highlightReceiverLists(): void { + this.highlightAsReceiver = true; + this.elementModel.connectedTo.forEach(connectedDropListID => { + DropListComponent.dragAndDropComponents[connectedDropListID].highlightAsReceiver = true; + }); + } + createDragImage(baseElement: HTMLElement, baseID: string): HTMLElement { const dragImage: HTMLElement = baseElement.cloneNode(true) as HTMLElement; dragImage.id = `${baseID}-dragimage`; - dragImage.style.display = 'block'; - dragImage.style.width = `${(baseElement as HTMLElement).offsetWidth}px`; + dragImage.style.display = 'inline-block'; + dragImage.style.maxWidth = `${(baseElement as HTMLElement).offsetWidth}px`; dragImage.style.fontSize = `${this.elementModel.styling.fontSize}px`; dragImage.style.borderRadius = '5px'; dragImage.style.padding = '10px'; @@ -154,9 +163,8 @@ export class DropListComponent extends FormElementComponent implements OnInit, A return dragImage; } - dragEnterItem(event: DragEvent) { + moveElementInSortList(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); @@ -195,38 +203,53 @@ export class DropListComponent extends FormElementComponent implements OnInit, A // 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); - if (!valueIDs.includes(DropListComponent.draggedElement?.id)) { - this.addDraggedElementToList(); + const isIDAlreadyPresent = valueIDs.includes(DropListComponent.draggedElement?.id); + if (!isIDAlreadyPresent) { + if (this.elementModel.onlyOneItem && + this.viewModel.length > 0 && + this.viewModel[0].returnToOriginOnReplacement) { + 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) { - DropListComponent.removeElementFromSourceList(); + DropListComponent.removeElementFromList(DropListComponent.sourceList as DropListComponent, + DropListComponent.sourceList?.placeHolderIndex as number); } } else if (this.elementModel.deleteDroppedItemWithSameID) { - DropListComponent.removeElementFromSourceList(); + DropListComponent.removeElementFromList(DropListComponent.sourceList as DropListComponent, + DropListComponent.sourceList?.placeHolderIndex as number); } } - // else { - // console.log('Not an allowed target list'); - // } this.dragEnd(); } - addDraggedElementToList(): void { - this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject); - this.elementFormControl.setValue(this.viewModel); + 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)); } - static removeElementFromSourceList(): void { - DropListComponent.sourceList?.viewModel.splice(DropListComponent.sourceList.placeHolderIndex as number, 1); - DropListComponent.sourceList?.elementFormControl.setValue(DropListComponent.sourceList.viewModel); + static addElementToList(listComponent: DropListComponent, element: DragNDropValueObject, targetIndex?: number): void { + if (targetIndex) { + listComponent.viewModel.splice( + Math.min(listComponent.viewModel.length, element.originListIndex || 0), + 0, + element + ); + } else { + listComponent.viewModel.push(element); + } + listComponent.elementFormControl.setValue(listComponent.viewModel); } - isDropAllowed(connectedDropLists: string[]): boolean { - return (DropListComponent.sourceList === this) || - ((connectedDropLists as string[]).includes(this.elementModel.id) && - !(this.elementModel.onlyOneItem && this.elementModel.value.length > 0)); - // TODO presentValueIDs? + static removeElementFromList(listComponent: DropListComponent, index: number): void { + listComponent.viewModel.splice(index, 1); + listComponent.elementFormControl.setValue(listComponent.viewModel); } dragEnd(event?: DragEvent) { diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index ece5e82b87373640ffff6a3d0be80dd320d4a5a2..f9f4aa35c13a1ae2acfa795bac414de24d8f4196 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -336,6 +336,9 @@ export interface TextImageLabel extends TextLabel { export interface DragNDropValueObject extends TextImageLabel { id: string; + returnToOriginOnReplacement?: boolean; + originListID?: string; + originListIndex?: number; } export type Label = TextLabel | TextImageLabel | DragNDropValueObject; diff --git a/projects/common/models/elements/input-elements/drop-list.ts b/projects/common/models/elements/input-elements/drop-list.ts index 4c2e71da1892246adc98956cc8a632dfd7653c02..a1f5a53475347ef7aad6cbd4a284cb5bedb6f0c0 100644 --- a/projects/common/models/elements/input-elements/drop-list.ts +++ b/projects/common/models/elements/input-elements/drop-list.ts @@ -3,7 +3,7 @@ import { InputElement, DragNDropValueObject, BasicStyles, PositionProperties, - AnswerScheme, AnswerSchemeValue, UIElement + AnswerScheme, AnswerSchemeValue, UIElement, UIElementValue } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { DropListComponent } from 'common/components/input-elements/drop-list.component'; @@ -50,6 +50,20 @@ export class DropListElement extends InputElement { }); } + /* Set originListID and originListIndex if applicable. */ + setProperty(property: string, value: UIElementValue): void { + super.setProperty(property, value); + if (property === 'value' || property === 'id') { + this.value.forEach((dndValue: DragNDropValueObject, index) => { + this.value[index] = { + ...dndValue, + originListID: dndValue.returnToOriginOnReplacement ? this.id : undefined, + originListIndex: dndValue.returnToOriginOnReplacement ? this.value.indexOf(dndValue) : undefined + }; + }); + } + } + hasAnswerScheme(): boolean { return Boolean(this.getAnswerScheme); } 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 387774c772833190f57333ac013f321def10fcf6..3f39d6df990845e66eba3f246524304a1b51c3d0 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 @@ -11,6 +11,9 @@ import { DragNDropValueObject } from 'common/models/elements/element'; <mat-label>{{'text' | translate }}</mat-label> <input #textField matInput type="text" [value]="data.value.text"> </mat-form-field> + <mat-checkbox #returnToOriginOnReplacement [checked]="$any(data.value.returnToOriginOnReplacement)"> + {{'propertiesPanel.returnToOriginOnReplacement' | translate }} + </mat-checkbox> <button mat-raised-button (click)="loadImage()">{{ 'loadImage' | translate }}</button> <button mat-raised-button (click)="imgSrc = null">{{ 'removeImage' | translate }}</button> <img [src]="imgSrc" @@ -25,8 +28,9 @@ import { DragNDropValueObject } from 'common/models/elements/element'; <button mat-button [mat-dialog-close]="{ text: textField.value, imgSrc: imgSrc, - id: idField.value - } "> + id: idField.value, + returnToOriginOnReplacement: returnToOriginOnReplacement.checked + }"> {{'save' | translate }} </button> <button mat-button mat-dialog-close>{{'cancel' | translate }}</button> 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 208cea5f558c92b12caa0127db34da7616f8aa07..debf1238233fa046b47e263e10382230e17b8e13 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 @@ -121,7 +121,8 @@ export class DropListPropertiesComponent { text: value, imgSrc: null, imgPosition: 'above', - id: this.unitService.getNewValueID() + id: this.unitService.getNewValueID(), + returnToOriginOnReplacement: false } ] }); @@ -135,17 +136,17 @@ export class DropListPropertiesComponent { } async editOption(optionIndex: number): Promise<void> { - const oldOptions: DragNDropValueObject[] = this.combinedProperties.value as DragNDropValueObject[]; + const dropListValues: DragNDropValueObject[] = this.combinedProperties.value as DragNDropValueObject[]; - await this.dialogService.showDropListOptionEditDialog(oldOptions[optionIndex]) + await this.dialogService.showDropListOptionEditDialog(dropListValues[optionIndex]) .subscribe((result: DragNDropValueObject) => { if (result) { - if (result.id !== oldOptions[optionIndex].id && !this.idManager.isIdAvailable(result.id)) { + if (result.id !== dropListValues[optionIndex].id && !this.idManager.isIdAvailable(result.id)) { this.messageService.showError(this.translateService.instant('idTaken')); return; } - oldOptions[optionIndex] = result; - this.updateModel.emit({ property: 'value', value: oldOptions }); + dropListValues[optionIndex] = result; + this.updateModel.emit({ property: 'value', value: dropListValues }); } }); }