chromium/ui/webui/resources/js/drag_wrapper.ts

// Copyright 2011 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview DragWrapper
 * A class for simplifying HTML5 drag and drop. Classes should use this to
 * handle the details of nested drag enters and leaves.
 */
export interface DragWrapperDelegate {
  // TODO(devlin): The only method this "delegate" actually needs is
  // shouldAcceptDrag(); the rest can be events emitted by the DragWrapper.
  /**
   * @return Whether the drag should be accepted. If false,
   *     subsequent methods (doDrag*) will not be called.
   */
  shouldAcceptDrag(e: MouseEvent): boolean;

  doDragEnter(e: MouseEvent): void;

  doDragLeave(e: MouseEvent): void;

  doDragOver(e: MouseEvent): void;

  doDrop(e: MouseEvent): void;
}

/**
 * Creates a DragWrapper which listens for drag target events on |target| and
 * delegates event handling to |delegate|.
 */
export class DragWrapper {
  /**
   * The number of un-paired dragenter events that have fired on |this|.
   * This is incremented by |onDragEnter_| and decremented by
   * |onDragLeave_|. This is necessary because dragging over child widgets
   * will fire additional enter and leave events on |this|. A non-zero value
   * does not necessarily indicate that |isCurrentDragTarget()| is true.
   */
  private dragEnters_: number = 0;
  private target_: HTMLElement;
  private delegate_: DragWrapperDelegate;

  constructor(target: HTMLElement, delegate: DragWrapperDelegate) {
    this.target_ = target;
    this.delegate_ = delegate;

    target.addEventListener('dragenter', e => this.onDragEnter_(e));
    target.addEventListener('dragover', e => this.onDragOver_(e));
    target.addEventListener('drop', e => this.onDrop_(e));
    target.addEventListener('dragleave', e => this.onDragLeave_(e));
  }

  /**
   * Whether the tile page is currently being dragged over with data it can
   * accept.
   */
  get isCurrentDragTarget(): boolean {
    return this.target_.classList.contains('drag-target');
  }

  /**
   * Delegate for dragenter events fired on |target_|.
   */
  private onDragEnter_(e: MouseEvent) {
    if (++this.dragEnters_ === 1) {
      if (this.delegate_.shouldAcceptDrag(e)) {
        this.target_.classList.add('drag-target');
        this.delegate_.doDragEnter(e);
      }
    } else {
      // Sometimes we'll get an enter event over a child element without an
      // over event following it. In this case we have to still call the
      // drag over delegate so that we make the necessary updates (one visible
      // symptom of not doing this is that the cursor's drag state will
      // flicker during drags).
      this.onDragOver_(e);
    }
  }

  /**
   * Thunk for dragover events fired on |target_|.
   */
  private onDragOver_(e: MouseEvent) {
    if (!this.target_.classList.contains('drag-target')) {
      return;
    }
    this.delegate_.doDragOver(e);
  }

  /**
   * Thunk for drop events fired on |target_|.
   */
  private onDrop_(e: MouseEvent) {
    this.dragEnters_ = 0;
    if (!this.target_.classList.contains('drag-target')) {
      return;
    }
    this.target_.classList.remove('drag-target');
    this.delegate_.doDrop(e);
  }

  /**
   * Thunk for dragleave events fired on |target_|.
   */
  private onDragLeave_(e: MouseEvent) {
    if (--this.dragEnters_ > 0) {
      return;
    }

    this.target_.classList.remove('drag-target');
    this.delegate_.doDragLeave(e);
  }
}