Javascript  |  213行  |  6.69 KB

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

// The delegate interface:
//   dragContainer -->
//         element containing the draggable items
//
//   transitionsDuration -->
//         length of time of transitions in ms
//
//   dragItem -->
//         get / set property containing the item being dragged
//
//   getItem(e) -->
//         get's the item that is under the mouse event |e|
//
//   canDropOn(coordinates) -->
//         returns true if the coordinates (relative to the drag container)
//         point to a valid place to drop an item
//
//   setDragPlaceholder(coordinates) -->
//         tells the delegate that the dragged item is currently above
//         the specified coordinates.
//
//   saveDrag(draggedItem) -->
//         tells the delegate that the drag is done. move the item to the
//         position last specified by setDragPlaceholder (e.g., commit changes).
//         draggedItem was the item being dragged.
//

// The distance, in px, that the mouse must move before initiating a drag.
var DRAG_THRESHOLD = 35;

function DragAndDropController(delegate) {
  this.delegate_ = delegate;

  // Install the 'mousedown' handler, the entry point to drag and drop.
  var el = this.delegate_.dragContainer;
  el.addEventListener('mousedown', this.handleMouseDown_.bind(this));
}

DragAndDropController.prototype = {
  isDragging_: false,
  startItem_: null,
  startItemXY_: null,
  startMouseXY_: null,
  mouseXY_: null,

  // Enables the handlers that are only active during a drag.
  enableHandlers_: function() {
    // Record references to the generated functions so we can
    // remove the listeners later.
    this.mouseMoveListener_ = this.handleMouseMove_.bind(this);
    this.mouseUpListener_ = this.handleMouseUp_.bind(this);
    this.scrollListener_ = this.handleScroll_.bind(this);

    document.addEventListener('mousemove', this.mouseMoveListener_, true);
    document.addEventListener('mouseup', this.mouseUpListener_, true);
    document.addEventListener('scroll', this.scrollListener_, true);
  },

  disableHandlers_: function() {
    document.removeEventListener('mousemove', this.mouseMoveListener_, true);
    document.removeEventListener('mouseup', this.mouseUpListener_, true);
    document.removeEventListener('scroll', this.scrollListener_, true);
  },

  isDragging: function() {
    return this.isDragging_;
  },

  distance_: function(p1, p2) {
    var x2 = Math.pow(p1.x - p2.x, 2);
    var y2 = Math.pow(p1.y - p2.y, 2);
    return Math.sqrt(x2 + y2);
  },

  // Shifts the client coordinates, |xy|, so they are relative to the top left
  // of the drag container.
  getCoordinates_: function(xy) {
    var rect = this.delegate_.dragContainer.getBoundingClientRect();
    var coordinates = {
      x: xy.x - rect.left,
      y: xy.y - rect.top
    };

    // If we're in an RTL language, reflect the coordinates so the delegate
    // doesn't need to worry about it.
    if (isRtl())
      coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x;

    return coordinates;
  },

  // Listen to mousedown to get the relative position of the cursor when
  // starting drag and drop.
  handleMouseDown_: function(e) {
    var item = this.delegate_.getItem(e);

    // This can't be a drag & drop event if it's not the left mouse button
    // or if the mouse is not above an item. We also bail out if the dragging
    // flag is still set (the flag remains around for a bit so that 'click'
    // event handlers can distinguish between a click and drag).
    if (!item || e.button != 0 || this.isDragging())
      return;

    this.startItem_ = item;
    this.startItemXY_ = {x: item.offsetLeft, y: item.offsetTop};
    this.startMouseXY_ = {x: e.clientX, y: e.clientY};
    this.startScrollXY_ = {x: window.scrollX, y: window.scrollY};

    this.enableHandlers_();
  },

  handleMouseMove_: function(e) {
    this.mouseXY_ = {x: e.clientX, y: e.clientY};

    if (this.isDragging()) {
      this.handleDrag_();
      return;
    }

    // Initiate the drag if the mouse has moved far enough.
    if (this.distance_(this.startMouseXY_, this.mouseXY_) >= DRAG_THRESHOLD)
      this.handleDragStart_();
  },

  handleMouseUp_: function() {
    this.handleDrop_();
  },

  handleScroll_: function(e) {
    if (this.isDragging())
      this.handleDrag_();
  },

  handleDragStart_: function() {
    // Use the item that the mouse was above when 'mousedown' fired.
    var item = this.startItem_;
    if (!item)
      return;

    this.isDragging_ = true;
    this.delegate_.dragItem = item;
    item.classList.add('dragging');
    item.style.zIndex = 2;
  },

  handleDragOver_: function() {
    var coordinates = this.getCoordinates_(this.mouseXY_);
    if (!this.delegate_.canDropOn(coordinates))
      return;

    this.delegate_.setDragPlaceholder(coordinates);
  },

  handleDrop_: function() {
    this.disableHandlers_();

    var dragItem = this.delegate_.dragItem;
    if (!dragItem)
      return;

    this.delegate_.dragItem = this.startItem_ = null;
    this.delegate_.saveDrag(dragItem);
    dragItem.classList.remove('dragging');

    setTimeout(function() {
      // Keep the flag around a little so other 'mouseup' and 'click'
      // listeners know the event is from a drag operation.
      this.isDragging_ = false;
      dragItem.style.zIndex = 0;
    }.bind(this), this.delegate_.transitionsDuration);
  },

  handleDrag_: function() {
    // Moves the drag item making sure that it is not displayed outside the
    // drag container.
    var dragItem = this.delegate_.dragItem;
    var dragContainer = this.delegate_.dragContainer;
    var rect = dragContainer.getBoundingClientRect();

    // First, move the item the same distance the mouse has moved.
    var x = this.startItemXY_.x + this.mouseXY_.x - this.startMouseXY_.x +
              window.scrollX - this.startScrollXY_.x;
    var y = this.startItemXY_.y + this.mouseXY_.y - this.startMouseXY_.y +
              window.scrollY - this.startScrollXY_.y;

    var w = this.delegate_.dimensions.width;
    var h = this.delegate_.dimensions.height;

    var offset = parseInt(getComputedStyle(dragContainer).marginLeft);

    // The position of the item is relative to the drag container. We
    // want to make sure that half of the item's width or height is within
    // the container.
    x = Math.max(x, - w / 2 - offset);
    x = Math.min(x, rect.width  + w / 2 - offset);

    y = Math.max(- h / 2, y);
    y = Math.min(y, rect.height - h / 2);

    dragItem.style.left = x + 'px';
    dragItem.style.top = y + 'px';

    // Update the layouts and positions based on the new drag location.
    this.handleDragOver_();

    this.delegate_.scrollPage(this.mouseXY_);
  }
};