Skip to content
Snippets Groups Projects
drop-list.component.ts 10.2 KiB
Newer Older
rhenck's avatar
rhenck committed
// eslint-disable-next-line max-classes-per-file
rhenck's avatar
rhenck committed
  AfterViewInit,
  Component, ElementRef, Input, OnDestroy, OnInit, Pipe, PipeTransform, ViewChild
} from '@angular/core';
import { DropListElement } from 'common/models/elements/input-elements/drop-list';
import { DragNDropValueObject } from 'common/models/elements/element';
import { FormElementComponent } from '../../directives/form-element-component.directive';
rhenck's avatar
rhenck committed

@Component({
  selector: 'aspect-drop-list',
rhenck's avatar
rhenck committed
  template: `
rhenck's avatar
rhenck committed
    <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"
         (drop)="drop($event)" (dragenter)="dragEnterList($event)" (dragleave)="dragLeaveList($event)"
         (dragover)="$event.preventDefault()">
      <ng-container *ngFor="let dropListValueElement of viewModel let index = index;">
        <div 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"
             draggable="true" [id]="dropListValueElement.id"
             (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)"
             [style.object-fit]="'scale-down'">
      </ng-container>
rhenck's avatar
rhenck committed
    </div>
rhenck's avatar
rhenck committed
    <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched"
               class="error-message">
      {{elementFormControl.errors | errorTransform: elementModel}}
    </mat-error>
rhenck's avatar
rhenck committed
  `,
  styles: [
rhenck's avatar
rhenck committed
    '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); padding: 3px;}',
    ':not(.clozeContext) .list-item {border-radius: 5px; padding: 10px;}',
    '.vertical-orientation .list-item:not(:last-child) {margin-bottom: 5px;}',
    '.horizontal-orientation .list-item:not(:last-child) {margin-right: 5px;}',
    '.errors {outline: 2px solid #f44336 !important;}',
rhenck's avatar
rhenck committed
    '.list-item {cursor: grab;}',
    '.list-item:active {cursor: grabbing}',
    '.show-as-placeholder {opacity: 0.5 !important; pointer-events: none;}',
    '.highlight-valid-drop {background-color: lightblue !important;}',
    '.highlight-as-receiver {outline: 2px solid;}',
    '.show-as-hidden {visibility: hidden;}'
rhenck's avatar
rhenck committed
  ]
})
rhenck's avatar
rhenck committed
export class DropListComponent extends FormElementComponent implements OnInit, AfterViewInit, OnDestroy {
  @Input() elementModel!: DropListElement;
rhenck's avatar
rhenck committed
  @Input() clozeContext: boolean = false;
  @ViewChild('placeholder') placeholder!: ElementRef<HTMLElement>;
  static dragAndDropComponents: { [id: string]: DropListComponent } = {};

  viewModel: DragNDropValueObject[] = [];
  placeHolderIndex?: number;
  highlightAsReceiver = false;

  dragging = false;

  showAsPlaceholder = false;
  hidePlaceholder = false;
  highlightValidDrop = false;

  static draggedElement?: DragNDropValueObject;
  static sourceList?: DropListComponent;
rhenck's avatar
rhenck committed

rhenck's avatar
rhenck committed
  ngOnInit() {
    super.ngOnInit();
    this.viewModel = [...this.elementFormControl.value];
rhenck's avatar
rhenck committed
  ngAfterViewInit() {
    DropListComponent.dragAndDropComponents[this.elementModel.id] = this;
rhenck's avatar
rhenck committed
  dragStart(dragEvent: DragEvent,
            dropListValueElement: DragNDropValueObject,
            sourceListIndex: number) {
    if (dragEvent.dataTransfer) {
      dragEvent.dataTransfer.effectAllowed = 'copyMove';
      dragEvent.dataTransfer.setDragImage(
        DropListComponent.createDragImage(dragEvent.target as Node, dropListValueElement.id), 0, 0);
    }

rhenck's avatar
rhenck committed
    // Sadly timeout is necessary for Chrome, which does not allow DOM manipulation on dragstart
    setTimeout(() => {
      DropListComponent.draggedElement = dropListValueElement;
      DropListComponent.sourceList = this;
      this.placeHolderIndex = sourceListIndex;
      if (this.elementModel.isSortList) {
        this.showAsPlaceholder = true;
      } else {
        this.hidePlaceholder = true;
        this.highlightValidDrop = true;
      }
rhenck's avatar
rhenck committed
      Object.entries(DropListComponent.dragAndDropComponents)
        .forEach(([, value]) => {
          value.dragging = true;
        });
rhenck's avatar
rhenck committed
      if (this.elementModel.highlightReceivingDropList) {
        this.highlightAsReceiver = true;
        this.elementModel.connectedTo.forEach(connectedDropListID => {
          DropListComponent.dragAndDropComponents[connectedDropListID].highlightAsReceiver = true;
        });
      }
    });
rhenck's avatar
rhenck committed
  }

  static createDragImage(baseElement: Node, baseID: string): HTMLElement {
    const dragImage: HTMLElement = baseElement.cloneNode(true) as HTMLElement;
    dragImage.id = `${baseID}-dragimage`;
    dragImage.style.display = 'inline-block';
    document.body.appendChild(dragImage);
    return dragImage;
  }

  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;
    }
  }

  dragEnterList(event: DragEvent) {
    event.preventDefault();

    if (!this.elementModel.isSortList) {
      this.highlightValidDrop = true;
    } 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);
      sourceList.placeHolderIndex = undefined;
      DropListComponent.sourceList = this;
      this.placeHolderIndex = this.viewModel.length > 0 ? this.viewModel.length - 1 : 0;
    }
  }

  dragLeaveList(event: DragEvent) {
    event.preventDefault();
    this.highlightValidDrop = false;
  }

  drop(event: DragEvent) {
    event.preventDefault();

    if (DropListComponent.sourceList === this && this.elementModel.isSortList) {
      this.elementFormControl.setValue(this.viewModel);
    } else if (this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) {
      const presentValueIDs = this.elementFormControl.value
        .map((valueValue: DragNDropValueObject) => valueValue.id);
      if (!presentValueIDs.includes(DropListComponent.draggedElement?.id)) {
        this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject);
        this.elementFormControl.setValue(this.viewModel);
        if (!DropListComponent.sourceList?.elementModel.copyOnDrop) {
          DropListComponent.sourceList?.viewModel.splice(DropListComponent.sourceList.placeHolderIndex as number, 1);
rhenck's avatar
rhenck committed
          DropListComponent.sourceList?.elementFormControl.setValue(DropListComponent.sourceList.viewModel);
        }
rhenck's avatar
rhenck committed
      }
rhenck's avatar
rhenck committed
    } else {
      console.log('Not an allowed target list');
rhenck's avatar
rhenck committed
    }
rhenck's avatar
rhenck committed
    this.dragEnd();
  }

  isDropAllowed(connectedDropLists: string[]): boolean {
    return (connectedDropLists as string[]).includes(this.elementModel.id) &&
           !(this.elementModel.onlyOneItem && this.elementModel.value.length > 0);
    // TODO presentValueIDs?
  }

  dragEnd(event?: DragEvent) {
    event?.preventDefault();

    Object.entries(DropListComponent.dragAndDropComponents)
      .forEach(([, value]) => {
        value.highlightAsReceiver = false;
        value.dragging = false;
        value.highlightValidDrop = false;
      });
    if (DropListComponent.sourceList) DropListComponent.sourceList.placeHolderIndex = undefined;
    this.placeHolderIndex = undefined;

    document.getElementById(`${DropListComponent.draggedElement?.id}-dragimage`)?.remove();
rhenck's avatar
rhenck committed
  }
rhenck's avatar
rhenck committed
  ngOnDestroy(): void {
    delete DropListComponent.dragAndDropComponents[this.elementModel.id];
rhenck's avatar
rhenck committed
@Pipe({
  name: 'droplistLayout'
})
export class DropListLayoutPipe implements PipeTransform {
  transform(orientation: string): string {
rhenck's avatar
rhenck committed
      case 'horizontal':
        return 'row';
      case 'vertical':
        return 'column';
      case 'flex':
        return 'row wrap';
      default:
        throw Error(`droplist orientation invalid: ${orientation}`);
rhenck's avatar
rhenck committed
@Pipe({
  name: 'droplistLayoutAlign'
})
export class DropListLayoutAlignPipe implements PipeTransform {
  transform(orientation: string): string {
    switch (orientation) {
      case 'horizontal':
        return 'start start';
      case 'vertical':
        return 'start stretch';
      case 'flex':
        return 'space-around center';
      default:
        throw Error(`droplist orientation invalid: ${orientation}`);
    }
  }
rhenck's avatar
rhenck committed
}