Skip to content
Snippets Groups Projects
drop-list.component.ts 13.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { Component, Input, OnInit } from '@angular/core';
    
      CdkDrag,
      CdkDragDrop,
      CdkDragStart,
      CdkDropList,
      moveItemInArray,
      transferArrayItem,
      copyArrayItem
    
    } from '@angular/cdk/drag-drop';
    
    import { DropListElement } from 'common/models/elements/input-elements/drop-list';
    
    import { FormElementComponent } from '../../directives/form-element-component.directive';
    
    rhenck's avatar
    rhenck committed
    
    
    import { DragNDropValueObject } from 'common/models/elements/label-interfaces';
    
    
    rhenck's avatar
    rhenck committed
    @Component({
    
      selector: 'aspect-drop-list',
    
    rhenck's avatar
    rhenck committed
      template: `
    
        <div class="list" [id]="elementModel.id"
    
    rhenck's avatar
    rhenck committed
             tabindex="0"
    
    rhenck's avatar
    rhenck committed
             [class.cloze-context]="clozeContext"
    
             [class.only-one-item]="elementModel.onlyOneItem"
    
             [class.vertical-orientation]="elementModel.orientation === 'vertical'"
             [class.horizontal-orientation]="elementModel.orientation === 'horizontal'"
    
    rhenck's avatar
    rhenck committed
             [class.floating-orientation]="elementModel.orientation === 'flex'"
    
             [class.static-placeholder]="parentForm && elementFormControl.value.length === 1 &&
                                        (elementModel.allowReplacement || (elementModel.copyOnDrop && showsPlaceholder)) &&
                                        !dragging"
    
             [class.highlight-receiver]="classReference.highlightReceivingDropList"
    
             [class.error]="elementFormControl.errors &&
                            elementFormControl.touched &&
                            !classReference.highlightReceivingDropList"
    
    rhenck's avatar
    rhenck committed
             cdkDropList
    
             (cdkDropListEntered)="showsPlaceholder = true"
             (cdkDropListExited)="showsPlaceholder = false"
    
    rhenck's avatar
    rhenck committed
             [cdkDropListData]="this" [cdkDropListConnectedTo]="elementModel.connectedTo"
    
             [cdkDropListOrientation]="$any(elementModel.orientation)"
    
             [cdkDropListSortingDisabled]="elementModel.orientation === 'flex'"
    
    rhenck's avatar
    rhenck committed
             [cdkDropListEnterPredicate]="validDropPredicate"
    
             (cdkDropListDropped)="drop($event);"
    
             [style.gap.px]="elementModel.onlyOneItem ? 0 : 5"
    
             [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"
    
             [style.border-color]="elementModel.highlightReceivingDropListColor"
    
    rhenck's avatar
    rhenck committed
             (focusout)="elementFormControl.markAsTouched()">
    
          <ng-container *ngFor="let dropListValueElement of
    
            parentForm ? elementFormControl.value : elementModel.value; let index = index;">
    
            <div *ngIf="!dropListValueElement.imgSrc"
    
                 class="list-item text-list-item"
                 [class.hide-list-item]="parentForm && (elementModel.allowReplacement || elementModel.copyOnDrop) &&
    
                                         elementFormControl.value.length === 1 && showsPlaceholder"
    
    rhenck's avatar
    rhenck committed
                 cdkDrag [cdkDragData]="dropListValueElement"
                 (cdkDragStarted)="dragStart($event)"
                 (cdkDragEnded)="dragEnd()"
    
                 [style.background-color]="elementModel.styling.itemBackgroundColor">
              <span>{{dropListValueElement.text}}</span>
    
              <ng-template cdkDragPreview matchSize>
    
                     [class.cloze-context-preview]="clozeContext"
    
                     [style.color]="elementModel.styling.fontColor"
    
    rhenck's avatar
    rhenck committed
                     [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.background-color]="elementModel.styling.itemBackgroundColor">
                  <span>{{dropListValueElement.text}}</span>
                </div>
              </ng-template>
    
            <div *ngIf="dropListValueElement.imgSrc"
    
                 class="list-item image-list-item"
    
                 [class.hide-list-item]="parentForm && (elementModel.allowReplacement || elementModel.copyOnDrop) &&
    
                                         elementFormControl.value.length === 1 && showsPlaceholder"
    
                 cdkDrag [cdkDragData]="dropListValueElement"
    
                 (cdkDragStarted)="dragStart($event)"
                 (cdkDragEnded)="dragEnd()">
    
              <span class="baseline-helper">&nbsp;</span>
    
              <img [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder">
    
              <ng-template cdkDragPreview matchSize>
    
                <div>
                  <span class="baseline-helper">&nbsp;</span>
                  <img *ngIf="dropListValueElement.imgSrc"
    
                     [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder">
    
          </ng-container>
    
    
          <div class="list-item text-list-item" *ngIf="parentForm ?
                                        (!elementFormControl.value.length && !showsPlaceholder) ||
                                        (elementFormControl.value.length === 1 && !showsPlaceholder && dragging) :
                                        !elementModel.value.length;">
            <span class="baseline-helper">&nbsp;</span>
            <mat-error *ngIf="elementFormControl.errors &&
                              elementFormControl.touched &&
                              !classReference.highlightReceivingDropList"
                       class="error-message">{{elementFormControl.errors | errorTransform: elementModel}}</mat-error>
          </div>
    
    rhenck's avatar
    rhenck committed
      `,
      styles: [
    
        '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); border-radius: 5px;}',
    
        '.list {display: flex; box-sizing: border-box; padding: 5px;}',
    
    rhenck's avatar
    rhenck committed
        '.list.vertical-orientation {flex-direction: column;}',
        '.list.horizontal-orientation {flex-direction: row;}',
        '.list.floating-orientation {place-content: center space-around; align-items: center; flex-flow: row wrap;}',
    
        '.cloze-context.list {padding: 0}',
        '.highlight-receiver.cdk-drop-list-receiving {border: 2px solid;}',
        '.highlight-receiver.cdk-drop-list-receiving:not(.cloze-context) {padding: 3px;}',
        '.error {padding: 3px; border: 2px solid #f44336 !important;}',
        '.list-item:active {cursor: grabbing;}',
    
    rhenck's avatar
    rhenck committed
        '.list-item {border-radius: 5px;}',
    
        ':not(.cloze-context) .list-item.text-list-item {padding: 10px;}',
        '.cloze-context .list-item.text-list-item {padding: 0 5px;}',
    
        '.cloze-context.only-one-item .list-item {height: 100%; display: flex; align-items: center; justify-content: center;}',
    
        '.image-list-item {align-self: flex-start;}',
    
        '.hide-list-item {display: none !important; transform: unset !important;}',
    
        '.cdk-drag-preview {border-radius: 5px; box-shadow: 2px 2px 5px black;}',
    
        '.cdk-drag-preview.text-preview {padding: 10px; box-sizing: border-box;}',
    
        '.cdk-drag-preview.cloze-context-preview {padding: 0 2px; display: flex; align-items: center; justify-content: center;}',
    
    rhenck's avatar
    rhenck committed
        '.cdk-drop-list-dragging .cdk-drag {transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}',
    
        '.cdk-drag-placeholder {background-color: #ccc !important;}',
        '.cdk-drag-placeholder {transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}',
    
        '.cdk-drag-placeholder * {visibility: hidden;}',
    
        '.static-placeholder .cdk-drag-placeholder {transform: unset !important;}',
        '.error-message {font-size: 75%;}',
        '.baseline-helper {width: 0; display: inline-block;}'
    
    rhenck's avatar
    rhenck committed
      ]
    })
    
    export class DropListComponent extends FormElementComponent implements OnInit {
    
      @Input() elementModel!: DropListElement;
    
    rhenck's avatar
    rhenck committed
      @Input() clozeContext: boolean = false;
    
      dragging = false;
      showsPlaceholder = false;
    
      classReference = DropListComponent;
    
      static highlightReceivingDropList = false;
    
      static dragAndDropComponents: { [id: string]: DropListComponent } = {};
    
      ngOnInit() {
        super.ngOnInit();
        DropListComponent.dragAndDropComponents[this.elementModel.id] = this;
      }
    
    
    rhenck's avatar
    rhenck committed
      dragStart(event: CdkDragStart) {
        DropListComponent.setHighlighting(event.source.dropContainer.data.elementModel.highlightReceivingDropList);
    
        // add class for cursor while dragging
    
    rhenck's avatar
    rhenck committed
        document.body.classList.add('dragging-active');
    
        this.showsPlaceholder = true;
    
        this.dragging = true;
    
    rhenck's avatar
    rhenck committed
      }
    
      dragEnd() {
        DropListComponent.setHighlighting(false);
        document.body.classList.remove('dragging-active');
    
        this.dragging = false;
    
        Object.values(DropListComponent.dragAndDropComponents).forEach(d => { d.showsPlaceholder = false; });
    
    rhenck's avatar
    rhenck committed
      }
    
      static setHighlighting(showHighlight: boolean) {
        DropListComponent.highlightReceivingDropList = showHighlight;
      }
    
    rhenck's avatar
    rhenck committed
      drop(event: CdkDragDrop<any>) {
    
    rhenck's avatar
    rhenck committed
        if (DropListComponent.isReorderDrop(event)) {
    
          moveItemInArray(event.container.data.elementFormControl.value, event.previousIndex, event.currentIndex);
    
    rhenck's avatar
    rhenck committed
          event.container.data.updateFormvalue();
    
        } else if (DropListComponent.isPutBack(event.item, event.container)) {
    
          event.previousContainer.data.elementFormControl.value.splice(event.previousIndex, 1);
    
    rhenck's avatar
    rhenck committed
          event.previousContainer.data.updateFormvalue();
    
        } else if (DropListComponent.isReplace(event)) {
    
          const isToReplaceItemAlreadyInOrigin: boolean =
    
            event.container.data.elementFormControl.value[0].originListID === event.container.data.elementModel.id;
    
          if (isToReplaceItemAlreadyInOrigin) {
    
          // splice first and hold the replaced item, then move. to prevent indix mixup
    
          const replacedItem: DragNDropValueObject = event.container.data.elementFormControl.value.splice(0, 1)[0];
    
          DropListComponent.moveItem(event);
    
          const originComponent = DropListComponent.dragAndDropComponents[replacedItem.originListID];
    
          const isIDAlreadyPresentInOrigin = DropListComponent.isItemIDAlreadyPresent(
            replacedItem.id,
            originComponent.elementFormControl.value);
          if (!(originComponent.elementModel.copyOnDrop && isIDAlreadyPresentInOrigin)) {
            DropListComponent.addElementToList(originComponent, replacedItem);
          }
        } else {
          DropListComponent.moveItem(event);
    
      static moveItem(event: CdkDragDrop<any>): void {
    
        if (DropListComponent.isCopyDrop(event)) {
    
          copyArrayItem(
            event.previousContainer.data.elementFormControl.value,
            event.container.data.elementFormControl.value,
            event.previousIndex,
            event.currentIndex);
    
          DropListComponent.transferItem(event.previousContainer, event.container, event.previousIndex, event.currentIndex);
        }
        event.previousContainer.data.updateFormvalue();
        event.container.data.updateFormvalue();
      }
    
    
      static transferItem(previousContainer: CdkDropList, newContainer: CdkDropList,
                          previousIndex: number, newIndex: number): void {
    
        transferArrayItem(
          previousContainer.data.elementFormControl.value,
          newContainer.data.elementFormControl.value,
          previousIndex,
          newIndex
        );
      }
    
      static addElementToList(listComponent: DropListComponent, element: DragNDropValueObject): void {
    
        const targetIndex = Math.min(listComponent.elementFormControl.value.length, element.originListIndex || 0);
        listComponent.elementFormControl.value.splice(targetIndex, 0, element);
    
        listComponent.elementFormControl.setValue(listComponent.elementFormControl.value);
    
    rhenck's avatar
    rhenck committed
      /* Move element within the same list to a new index position. */
    
    rhenck's avatar
    rhenck committed
      static isReorderDrop(event: CdkDragDrop<any>): boolean {
        return event.previousContainer === event.container;
      }
    
      static isCopyDrop(event: CdkDragDrop<any>): boolean {
        return event.previousContainer.data.elementModel.copyOnDrop;
      }
    
    
    rhenck's avatar
    rhenck committed
      /* Put a copied element back to the source list. */
    
      static isPutBack(draggedItem: CdkDrag, list: CdkDropList): boolean {
        return list.data.elementModel.copyOnDrop &&
          DropListComponent.isItemIDAlreadyPresent(draggedItem.data.id, list.data.elementFormControl.value);
    
      static isReplace(event: CdkDragDrop<any>): boolean {
        return event.container.data.elementFormControl.value.length === 1 &&
          event.container.data.elementModel.allowReplacement;
      }
    
    
      static isItemIDAlreadyPresent(itemID: string, valueList: DragNDropValueObject[]): boolean {
        const listValueIDs = valueList.map((valueValue: DragNDropValueObject) => valueValue.id);
        return listValueIDs.includes(itemID);
      }
    
    
    rhenck's avatar
    rhenck committed
      updateFormvalue(): void {
    
        this.elementFormControl.setValue(this.elementFormControl.value);
    
      validDropPredicate = (draggedItem: CdkDrag, targetList: CdkDropList): boolean => {
        if (!DropListComponent.isItemIDAlreadyPresent(draggedItem.data.id, targetList.data.elementFormControl.value) &&
           !targetList.data.elementModel.onlyOneItem) {
          return true;
        }
    
        if (targetList.data.elementModel.onlyOneItem && targetList.data.elementFormControl.value.length < 1) {
          return true;
        }
    
        if (targetList.data.elementModel.onlyOneItem &&
            targetList.data.elementFormControl.value.length > 0 &&
            (targetList.data.elementModel.allowReplacement && DropListComponent.containedItemIsReplacable(targetList))) {
          return true;
        }
    
        if (DropListComponent.isItemIDAlreadyPresent(draggedItem.data.id, targetList.data.elementFormControl.value) &&
            targetList.data.elementModel.onlyOneItem &&
            (targetList.data.elementModel.allowReplacement && DropListComponent.containedItemIsReplacable(targetList))) {
          return true;
        }
    
        if (DropListComponent.isPutBack(draggedItem, targetList)) {
          return true;
        }
        return false;
      };
    
    
      /* To be replacable an item must not be in it's origin. Otherwise it has nowhere to go to. */
      static containedItemIsReplacable(list: CdkDropList): boolean {
        return list.data.elementFormControl.value[0].originListID !== list.data.elementModel.id;
      }
    
    rhenck's avatar
    rhenck committed
    }