File

projects/angular-draggable-droppable/src/lib/droppable.directive.ts

Extends

DropEvent

import {
  Directive,
  OnInit,
  ElementRef,
  OnDestroy,
  Output,
  EventEmitter,
  NgZone,
  Input,
  Renderer2,
  Optional,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { distinctUntilChanged, pairwise, filter, map } from 'rxjs/operators';
import { DraggableHelper } from './draggable-helper.provider';
import { DraggableScrollContainerDirective } from './draggable-scroll-container.directive';
import { addClass, removeClass } from './util';

function isCoordinateWithinRectangle(
  clientX: number,
  clientY: number,
  rect: ClientRect
): boolean {
  return (
    clientX >= rect.left &&
    clientX <= rect.right &&
    clientY >= rect.top &&
    clientY <= rect.bottom
  );
}

export interface DropEvent<T = any> {
  dropData: T;
  /**
   * ClientX value of the mouse location where the drop occurred
   */
  clientX: number;
  /**
   * ClientY value of the mouse location where the drop occurred
   */
  clientY: number;
  /**
   * The target of the event where the drop occurred
   */
  target: EventTarget;
}

export interface DragEvent<T = any> extends DropEvent<T> {}

export interface ValidateDropParams extends DropEvent {}

export type ValidateDrop = (params: ValidateDropParams) => boolean;

@Directive({
  selector: '[mwlDroppable]',
})
export class DroppableDirective implements OnInit, OnDestroy {
  /**
   * Added to the element when an element is dragged over it
   */
  @Input() dragOverClass: string;

  /**
   * Added to the element any time a draggable element is being dragged
   */
  @Input() dragActiveClass: string;

  /**
   * Allow custom behaviour to control when the element is dropped
   */
  @Input() validateDrop: ValidateDrop;

  /**
   * Called when a draggable element starts overlapping the element
   */
  @Output() dragEnter = new EventEmitter<DropEvent>();

  /**
   * Called when a draggable element stops overlapping the element
   */
  @Output() dragLeave = new EventEmitter<DropEvent>();

  /**
   * Called when a draggable element is moved over the element
   */
  @Output() dragOver = new EventEmitter<DropEvent>();

  /**
   * Called when a draggable element is dropped on this element
   */
  @Output() drop = new EventEmitter<DropEvent>(); // eslint-disable-line  @angular-eslint/no-output-native

  currentDragSubscription: Subscription;

  constructor(
    private element: ElementRef<HTMLElement>,
    private draggableHelper: DraggableHelper,
    private zone: NgZone,
    private renderer: Renderer2,
    @Optional() private scrollContainer: DraggableScrollContainerDirective
  ) {}

  ngOnInit() {
    this.currentDragSubscription = this.draggableHelper.currentDrag.subscribe(
      (drag$) => {
        addClass(this.renderer, this.element, this.dragActiveClass);
        const droppableElement: {
          rect?: ClientRect;
          updateCache: boolean;
          scrollContainerRect?: ClientRect;
        } = {
          updateCache: true,
        };

        const deregisterScrollListener = this.renderer.listen(
          this.scrollContainer
            ? this.scrollContainer.elementRef.nativeElement
            : 'window',
          'scroll',
          () => {
            droppableElement.updateCache = true;
          }
        );

        let currentDragEvent: DragEvent;
        const overlaps$ = drag$.pipe(
          map(({ clientX, clientY, dropData, target }) => {
            currentDragEvent = { clientX, clientY, dropData, target };
            if (droppableElement.updateCache) {
              droppableElement.rect =
                this.element.nativeElement.getBoundingClientRect();
              if (this.scrollContainer) {
                droppableElement.scrollContainerRect =
                  this.scrollContainer.elementRef.nativeElement.getBoundingClientRect();
              }
              droppableElement.updateCache = false;
            }
            const isWithinElement = isCoordinateWithinRectangle(
              clientX,
              clientY,
              droppableElement.rect as ClientRect
            );

            const isDropAllowed =
              !this.validateDrop ||
              this.validateDrop({ clientX, clientY, target, dropData });

            if (droppableElement.scrollContainerRect) {
              return (
                isWithinElement &&
                isDropAllowed &&
                isCoordinateWithinRectangle(
                  clientX,
                  clientY,
                  droppableElement.scrollContainerRect as ClientRect
                )
              );
            } else {
              return isWithinElement && isDropAllowed;
            }
          })
        );

        const overlapsChanged$ = overlaps$.pipe(distinctUntilChanged());

        let dragOverActive: boolean; // TODO - see if there's a way of doing this via rxjs

        overlapsChanged$
          .pipe(filter((overlapsNow) => overlapsNow))
          .subscribe(() => {
            dragOverActive = true;
            addClass(this.renderer, this.element, this.dragOverClass);
            if (this.dragEnter.observers.length > 0) {
              this.zone.run(() => {
                this.dragEnter.next(currentDragEvent);
              });
            }
          });

        overlaps$.pipe(filter((overlapsNow) => overlapsNow)).subscribe(() => {
          if (this.dragOver.observers.length > 0) {
            this.zone.run(() => {
              this.dragOver.next(currentDragEvent);
            });
          }
        });

        overlapsChanged$
          .pipe(
            pairwise(),
            filter(([didOverlap, overlapsNow]) => didOverlap && !overlapsNow)
          )
          .subscribe(() => {
            dragOverActive = false;
            removeClass(this.renderer, this.element, this.dragOverClass);
            if (this.dragLeave.observers.length > 0) {
              this.zone.run(() => {
                this.dragLeave.next(currentDragEvent);
              });
            }
          });

        drag$.subscribe({
          complete: () => {
            deregisterScrollListener();
            removeClass(this.renderer, this.element, this.dragActiveClass);
            if (dragOverActive) {
              removeClass(this.renderer, this.element, this.dragOverClass);
              if (this.drop.observers.length > 0) {
                this.zone.run(() => {
                  this.drop.next(currentDragEvent);
                });
              }
            }
          },
        });
      }
    );
  }

  ngOnDestroy() {
    if (this.currentDragSubscription) {
      this.currentDragSubscription.unsubscribe();
    }
  }
}

results matching ""

    No results matching ""