// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A DragListGroup is a class representing a group of one or more
 * "drag lists" with items that can be dragged within them and between them.
 *
 * @see ../demos/draglistgroup.html
 */


goog.provide('goog.fx.DragListDirection');
goog.provide('goog.fx.DragListGroup');
goog.provide('goog.fx.DragListGroup.EventType');
goog.provide('goog.fx.DragListGroupEvent');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventId');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.fx.Dragger');
goog.require('goog.math.Coordinate');
goog.require('goog.string');
goog.require('goog.style');



/**
 * A class representing a group of one or more "drag lists" with items that can
 * be dragged within them and between them.
 *
 * Example usage:
 *   var dragListGroup = new goog.fx.DragListGroup();
 *   dragListGroup.setDragItemHandleHoverClass(className1, className2);
 *   dragListGroup.setDraggerElClass(className3);
 *   dragListGroup.addDragList(vertList, goog.fx.DragListDirection.DOWN);
 *   dragListGroup.addDragList(horizList, goog.fx.DragListDirection.RIGHT);
 *   dragListGroup.init();
 *
 * @extends {goog.events.EventTarget}
 * @constructor
 * @struct
 */
goog.fx.DragListGroup = function() {
  goog.fx.DragListGroup.base(this, 'constructor');

  /**
   * The user-supplied CSS classes to add to a drag item on hover (not during a
   * drag action).
   * @private {Array|undefined}
   */
  this.dragItemHoverClasses_;

  /**
   * The user-supplied CSS classes to add to a drag item handle on hover (not
   * during a drag action).
   * @private {Array|undefined}
   */
  this.dragItemHandleHoverClasses_;

  /**
   * The user-supplied CSS classes to add to the current drag item (during a
   * drag action).
   * @private {Array|undefined}
   */
  this.currDragItemClasses_;

  /**
   * The user-supplied CSS classes to add to the clone of the current drag item
   * that's actually being dragged around (during a drag action).
   * @private {Array<string>|undefined}
   */
  this.draggerElClasses_;

  /**
   * The current drag item being moved.
   * Note: This is only defined while a drag action is happening.
   * @private {Element}
   */
  this.currDragItem_;

  /**
   * The drag list that {@code this.currDragItem_} is currently hovering over,
   * or null if it is not hovering over a list.
   * @private {Element}
   */
  this.currHoverList_;

  /**
   * The original drag list that the current drag item came from. We need to
   * remember this in case the user drops the item outside of any lists, in
   * which case we return the item to its original location.
   * Note: This is only defined while a drag action is happening.
   * @private {Element}
   */
  this.origList_;

  /**
   * The original next item in the original list that the current drag item came
   * from. We need to remember this in case the user drops the item outside of
   * any lists, in which case we return the item to its original location.
   * Note: This is only defined while a drag action is happening.
   * @private {Element}
   */
  this.origNextItem_;

  /**
   * The current item in the list we are hovering over. We need to remember
   * this in case we do not update the position of the current drag item while
   * dragging (see {@code updateWhileDragging_}). In this case the current drag
   * item will be inserted into the list before this element when the drag ends.
   * @private {Element}
   */
  this.currHoverItem_;

  /**
   * The clone of the current drag item that's actually being dragged around.
   * Note: This is only defined while a drag action is happening.
   * @private {HTMLElement}
   */
  this.draggerEl_;

  /**
   * The dragger object.
   * Note: This is only defined while a drag action is happening.
   * @private {goog.fx.Dragger}
   */
  this.dragger_;

  /**
   * The amount of distance, in pixels, after which a mousedown or touchstart is
   * considered a drag.
   * @private {number}
   */
  this.hysteresisDistance_ = 0;


  /**
   * The drag lists.
   * @private {Array<Element>}
   */
  this.dragLists_ = [];

  /**
   * All the drag items. Set by init().
   * @private {Array<Element>}
   */
  this.dragItems_ = [];

  /**
   * Which drag item corresponds to a given handle.  Set by init().
   * Specifically, this maps from the unique ID (as given by goog.getUid)
   * of the handle to the drag item.
   * @private {Object}
   */
  this.dragItemForHandle_ = {};

  /**
   * The event handler for this instance.
   * @private {goog.events.EventHandler<!goog.fx.DragListGroup>}
   */
  this.eventHandler_ = new goog.events.EventHandler(this);

  /**
   * Whether the setup has been done to make all items in all lists draggable.
   * @private {boolean}
   */
  this.isInitialized_ = false;

  /**
   * Whether the currDragItem is always displayed. By default the list
   * collapses, the currDragItem's display is set to none, when we do not
   * hover over a draglist.
   * @private {boolean}
   */
  this.isCurrDragItemAlwaysDisplayed_ = false;

  /**
   * Whether to update the position of the currDragItem as we drag, i.e.,
   * insert the currDragItem each time to the position where it would land if
   * we were to end the drag at that point. Defaults to true.
   * @private {boolean}
   */
  this.updateWhileDragging_ = true;
};
goog.inherits(goog.fx.DragListGroup, goog.events.EventTarget);


/**
 * Enum to indicate the direction that a drag list grows.
 * @enum {number}
 */
goog.fx.DragListDirection = {
  DOWN: 0,      // common
  RIGHT: 2,     // common
  LEFT: 3,      // uncommon (except perhaps for right-to-left interfaces)
  RIGHT_2D: 4,  // common + handles multiple lines if items are wrapped
  LEFT_2D: 5    // for rtl languages
};


/**
 * Events dispatched by this class.
 * @enum {!goog.events.EventId<!goog.fx.DragListGroupEvent>}
 */
goog.fx.DragListGroup.EventType = {
  BEFOREDRAGSTART: new goog.events.EventId('beforedragstart'),
  DRAGSTART: new goog.events.EventId('dragstart'),
  BEFOREDRAGMOVE: new goog.events.EventId('beforedragmove'),
  DRAGMOVE: new goog.events.EventId('dragmove'),
  BEFOREDRAGEND: new goog.events.EventId('beforedragend'),
  DRAGEND: new goog.events.EventId('dragend')
};


/**
 * Sets the property of the currDragItem that it is always displayed in the
 * list.
 */
goog.fx.DragListGroup.prototype.setIsCurrDragItemAlwaysDisplayed = function() {
  this.isCurrDragItemAlwaysDisplayed_ = true;
};


/**
 * Sets the private property updateWhileDragging_ to false. This disables the
 * update of the position of the currDragItem while dragging. It will only be
 * placed to its new location once the drag ends.
 */
goog.fx.DragListGroup.prototype.setNoUpdateWhileDragging = function() {
  this.updateWhileDragging_ = false;
};


/**
 * Sets the distance the user has to drag the element before a drag operation
 * is started.
 * @param {number} distance The number of pixels after which a mousedown and
 *     move is considered a drag.
 */
goog.fx.DragListGroup.prototype.setHysteresis = function(distance) {
  this.hysteresisDistance_ = distance;
};


/**
 * @return {number} distance The number of pixels after which a mousedown and
 *     move is considered a drag.
 */
goog.fx.DragListGroup.prototype.getHysteresis = function() {
  return this.hysteresisDistance_;
};


/** @return {boolean} true if the user is currently dragging an element. */
goog.fx.DragListGroup.prototype.isDragging = function() {
  return !!this.dragger_;
};


/**
 * Adds a drag list to this DragListGroup.
 * All calls to this method must happen before the call to init().
 * Remember that all child nodes (except text nodes) will be made draggable to
 * any other drag list in this group.
 *
 * @param {Element} dragListElement Must be a container for a list of items
 *     that should all be made draggable.
 * @param {goog.fx.DragListDirection} growthDirection The direction that this
 *     drag list grows in (i.e. if an item is appended to the DOM, the list's
 *     bounding box expands in this direction).
 * @param {boolean=} opt_unused Unused argument.
 * @param {string=} opt_dragHoverClass CSS class to apply to this drag list when
 *     the draggerEl hovers over it during a drag action.  If present, must be a
 *     single, valid classname (not a string of space-separated classnames).
 */
goog.fx.DragListGroup.prototype.addDragList = function(
    dragListElement, growthDirection, opt_unused, opt_dragHoverClass) {
  goog.asserts.assert(!this.isInitialized_);

  dragListElement.dlgGrowthDirection_ = growthDirection;
  dragListElement.dlgDragHoverClass_ = opt_dragHoverClass;
  this.dragLists_.push(dragListElement);
};


/**
 * Sets a user-supplied function used to get the "handle" element for a drag
 * item. The function must accept exactly one argument. The argument may be
 * any drag item element.
 *
 * If not set, the default implementation uses the whole drag item as the
 * handle.
 *
 * @param {function(!Element): Element} getHandleForDragItemFn A function that,
 *     given any drag item, returns a reference to its "handle" element
 *     (which may be the drag item element itself).
 */
goog.fx.DragListGroup.prototype.setFunctionToGetHandleForDragItem = function(
    getHandleForDragItemFn) {
  goog.asserts.assert(!this.isInitialized_);
  this.getHandleForDragItem_ = getHandleForDragItemFn;
};


/**
 * Sets a user-supplied CSS class to add to a drag item on hover (not during a
 * drag action).
 * @param {...string} var_args The CSS class or classes.
 */
goog.fx.DragListGroup.prototype.setDragItemHoverClass = function(var_args) {
  goog.asserts.assert(!this.isInitialized_);
  this.dragItemHoverClasses_ = goog.array.slice(arguments, 0);
};


/**
 * Sets a user-supplied CSS class to add to a drag item handle on hover (not
 * during a drag action).
 * @param {...string} var_args The CSS class or classes.
 */
goog.fx.DragListGroup.prototype.setDragItemHandleHoverClass = function(
    var_args) {
  goog.asserts.assert(!this.isInitialized_);
  this.dragItemHandleHoverClasses_ = goog.array.slice(arguments, 0);
};


/**
 * Sets a user-supplied CSS class to add to the current drag item (during a
 * drag action).
 *
 * If not set, the default behavior adds visibility:hidden to the current drag
 * item so that it is a block of empty space in the hover drag list (if any).
 * If this class is set by the user, then the default behavior does not happen
 * (unless, of course, the class also contains visibility:hidden).
 *
 * @param {...string} var_args The CSS class or classes.
 */
goog.fx.DragListGroup.prototype.setCurrDragItemClass = function(var_args) {
  goog.asserts.assert(!this.isInitialized_);
  this.currDragItemClasses_ = goog.array.slice(arguments, 0);
};


/**
 * Sets a user-supplied CSS class to add to the clone of the current drag item
 * that's actually being dragged around (during a drag action).
 * @param {string} draggerElClass The CSS class.
 */
goog.fx.DragListGroup.prototype.setDraggerElClass = function(draggerElClass) {
  goog.asserts.assert(!this.isInitialized_);
  // Split space-separated classes up into an array.
  this.draggerElClasses_ = goog.string.trim(draggerElClass).split(' ');
};


/**
 * Performs the initial setup to make all items in all lists draggable.
 */
goog.fx.DragListGroup.prototype.init = function() {
  if (this.isInitialized_) {
    return;
  }

  for (var i = 0, numLists = this.dragLists_.length; i < numLists; i++) {
    var dragList = this.dragLists_[i];

    var dragItems = goog.dom.getChildren(dragList);
    for (var j = 0, numItems = dragItems.length; j < numItems; ++j) {
      this.listenForDragEvents(dragItems[j]);
    }
  }

  this.isInitialized_ = true;
};


/**
 * Adds a single item to the given drag list and sets up the drag listeners for
 * it.
 * If opt_index is specified the item is inserted at this index, otherwise the
 * item is added as the last child of the list.
 *
 * @param {!Element} list The drag list where to add item to.
 * @param {!Element} item The new element to add.
 * @param {number=} opt_index Index where to insert the item in the list. If not
 * specified item is inserted as the last child of list.
 */
goog.fx.DragListGroup.prototype.addItemToDragList = function(
    list, item, opt_index) {
  if (goog.isDef(opt_index)) {
    goog.dom.insertChildAt(list, item, opt_index);
  } else {
    goog.dom.appendChild(list, item);
  }
  this.listenForDragEvents(item);
};


/** @override */
goog.fx.DragListGroup.prototype.disposeInternal = function() {
  this.eventHandler_.dispose();

  for (var i = 0, n = this.dragLists_.length; i < n; i++) {
    var dragList = this.dragLists_[i];
    // Note: IE doesn't allow 'delete' for fields on HTML elements (because
    // they're not real JS objects in IE), so we just set them to undefined.
    dragList.dlgGrowthDirection_ = undefined;
    dragList.dlgDragHoverClass_ = undefined;
  }

  this.dragLists_.length = 0;
  this.dragItems_.length = 0;
  this.dragItemForHandle_ = null;

  // In the case where a drag event is currently in-progress and dispose is
  // called, this cleans up the extra state.
  this.cleanupDragDom_();

  goog.fx.DragListGroup.superClass_.disposeInternal.call(this);
};


/**
 * Caches the heights of each drag list and drag item, except for the current
 * drag item.
 *
 */
goog.fx.DragListGroup.prototype.recacheListAndItemBounds = function() {
  this.recacheListAndItemBounds_(this.currDragItem_);
};


/**
 * Caches the heights of each drag list and drag item, except for the current
 * drag item.
 *
 * @param {Element} currDragItem The item currently being dragged.
 * @private
 */
goog.fx.DragListGroup.prototype.recacheListAndItemBounds_ = function(
    currDragItem) {
  for (var i = 0, n = this.dragLists_.length; i < n; i++) {
    var dragList = this.dragLists_[i];
    dragList.dlgBounds_ = goog.style.getBounds(dragList);
  }

  for (var i = 0, n = this.dragItems_.length; i < n; i++) {
    var dragItem = this.dragItems_[i];
    if (dragItem != currDragItem) {
      dragItem.dlgBounds_ = goog.style.getBounds(dragItem);
    }
  }
};


/**
 * Listens for drag events on the given drag item. This method is currently used
 * to initialize drag items.
 *
 * @param {!Element} dragItem the element to initialize. This element has to be
 * in one of the drag lists.
 * @protected
 */
goog.fx.DragListGroup.prototype.listenForDragEvents = function(dragItem) {
  var dragItemHandle = this.getHandleForDragItem_(dragItem);
  var uid = goog.getUid(dragItemHandle);
  this.dragItemForHandle_[uid] = dragItem;

  if (this.dragItemHoverClasses_) {
    this.eventHandler_.listen(
        dragItem, goog.events.EventType.MOUSEOVER,
        this.handleDragItemMouseover_);
    this.eventHandler_.listen(
        dragItem, goog.events.EventType.MOUSEOUT, this.handleDragItemMouseout_);
  }
  if (this.dragItemHandleHoverClasses_) {
    this.eventHandler_.listen(
        dragItemHandle, goog.events.EventType.MOUSEOVER,
        this.handleDragItemHandleMouseover_);
    this.eventHandler_.listen(
        dragItemHandle, goog.events.EventType.MOUSEOUT,
        this.handleDragItemHandleMouseout_);
  }

  this.dragItems_.push(dragItem);
  this.eventHandler_.listen(
      dragItemHandle,
      [goog.events.EventType.MOUSEDOWN, goog.events.EventType.TOUCHSTART],
      this.handlePotentialDragStart_);
};


/**
 * Handles mouse and touch events which may start a drag action.
 * @param {!goog.events.BrowserEvent} e MOUSEDOWN or TOUCHSTART event.
 * @private
 */
goog.fx.DragListGroup.prototype.handlePotentialDragStart_ = function(e) {
  var uid = goog.getUid(/** @type {Node} */ (e.currentTarget));
  this.currDragItem_ = /** @type {Element} */ (this.dragItemForHandle_[uid]);

  this.draggerEl_ = /** @type {!HTMLElement} */ (
      this.createDragElementInternal(this.currDragItem_));
  if (this.draggerElClasses_) {
    // Add CSS class for the clone, if any.
    goog.dom.classlist.addAll(
        goog.asserts.assert(this.draggerEl_), this.draggerElClasses_ || []);
  }

  // Place the clone (i.e. draggerEl) at the same position as the actual
  // current drag item. This is a bit tricky since
  //   goog.style.getPageOffset() gets the left-top pos of the border, but
  //   goog.style.setPageOffset() sets the left-top pos of the margin.
  // It's difficult to adjust for the margins of the clone because it's
  // difficult to read it: goog.style.getComputedStyle() doesn't work for IE.
  // Instead, our workaround is simply to set the clone's margins to 0px.
  this.draggerEl_.style.margin = '0';
  this.draggerEl_.style.position = 'absolute';
  this.draggerEl_.style.visibility = 'hidden';
  var doc = goog.dom.getOwnerDocument(this.currDragItem_);
  doc.body.appendChild(this.draggerEl_);

  // Important: goog.style.setPageOffset() only works correctly for IE when the
  // element is already in the document.
  var currDragItemPos = goog.style.getPageOffset(this.currDragItem_);
  goog.style.setPageOffset(this.draggerEl_, currDragItemPos);

  this.dragger_ = new goog.fx.Dragger(this.draggerEl_);
  this.dragger_.setHysteresis(this.hysteresisDistance_);

  // Listen to events on the dragger. These handlers will be unregistered at
  // DRAGEND, when the dragger is disposed of. We can't use eventHandler_,
  // because it creates new references to the handler functions at each
  // dragging action, and keeps them until DragListGroup is disposed of.
  goog.events.listen(
      this.dragger_, goog.fx.Dragger.EventType.START, this.handleDragStart_,
      false, this);
  goog.events.listen(
      this.dragger_, goog.fx.Dragger.EventType.END, this.handleDragEnd_, false,
      this);
  goog.events.listen(
      this.dragger_, goog.fx.Dragger.EventType.EARLY_CANCEL, this.cleanup_,
      false, this);
  this.dragger_.startDrag(e);
};


/**
 * Creates copy of node being dragged.
 *
 * @param {Element} sourceEl Element to copy.
 * @return {!Element} The clone of {@code sourceEl}.
 * @deprecated Use goog.fx.Dragger.cloneNode().
 * @private
 */
goog.fx.DragListGroup.prototype.cloneNode_ = function(sourceEl) {
  return goog.fx.Dragger.cloneNode(sourceEl);
};


/**
 * Generates an element to follow the cursor during dragging, given a drag
 * source element.  The default behavior is simply to clone the source element,
 * but this may be overridden in subclasses.  This method is called by
 * {@code createDragElement()} before the drag class is added.
 *
 * @param {Element} sourceEl Drag source element.
 * @return {!Element} The new drag element.
 * @protected
 * @suppress {deprecated}
 */
goog.fx.DragListGroup.prototype.createDragElementInternal = function(sourceEl) {
  return this.cloneNode_(sourceEl);
};


/**
 * Handles the start of a drag action.
 * @param {!goog.fx.DragEvent} e goog.fx.Dragger.EventType.START event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragStart_ = function(e) {
  if (!this.dispatchEvent(
          new goog.fx.DragListGroupEvent(
              goog.fx.DragListGroup.EventType.BEFOREDRAGSTART, this,
              e.browserEvent, this.currDragItem_, null, null))) {
    e.preventDefault();
    this.cleanup_();
    return;
  }

  // Record the original location of the current drag item.
  // Note: this.origNextItem_ may be null.
  this.origList_ = /** @type {Element} */ (this.currDragItem_.parentNode);
  this.origNextItem_ = goog.dom.getNextElementSibling(this.currDragItem_);
  this.currHoverItem_ = this.origNextItem_;
  this.currHoverList_ = this.origList_;

  // If there's a CSS class specified for the current drag item, add it.
  // Otherwise, make the actual current drag item hidden (takes up space).
  if (this.currDragItemClasses_) {
    goog.dom.classlist.addAll(
        goog.asserts.assert(this.currDragItem_),
        this.currDragItemClasses_ || []);
  } else {
    this.currDragItem_.style.visibility = 'hidden';
  }

  // Precompute distances from top-left corner to center for efficiency.
  var draggerElSize = goog.style.getSize(this.draggerEl_);
  this.draggerEl_.halfWidth = draggerElSize.width / 2;
  this.draggerEl_.halfHeight = draggerElSize.height / 2;

  this.draggerEl_.style.visibility = '';

  // Record the bounds of all the drag lists and all the other drag items. This
  // caching is for efficiency, so that we don't have to recompute the bounds on
  // each drag move. Do this in the state where the current drag item is not in
  // any of the lists, except when update while dragging is disabled, as in this
  // case the current drag item does not get removed until drag ends.
  if (this.updateWhileDragging_) {
    this.currDragItem_.style.display = 'none';
  }
  this.recacheListAndItemBounds_(this.currDragItem_);
  this.currDragItem_.style.display = '';

  // Listen to events on the dragger.
  goog.events.listen(
      this.dragger_, goog.fx.Dragger.EventType.DRAG, this.handleDragMove_,
      false, this);

  this.dispatchEvent(
      new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.DRAGSTART, this, e.browserEvent,
          this.currDragItem_, this.draggerEl_, this.dragger_));
};


/**
 * Handles a drag movement (i.e. DRAG event fired by the dragger).
 *
 * @param {goog.fx.DragEvent} dragEvent Event object fired by the dragger.
 * @return {boolean} The return value for the event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragMove_ = function(dragEvent) {

  // Compute the center of the dragger element (i.e. the cloned drag item).
  var draggerElPos = goog.style.getPageOffset(this.draggerEl_);
  var draggerElCenter = new goog.math.Coordinate(
      draggerElPos.x + this.draggerEl_.halfWidth,
      draggerElPos.y + this.draggerEl_.halfHeight);

  // Check whether the center is hovering over one of the drag lists.
  var hoverList = this.getHoverDragList_(draggerElCenter);

  // If hovering over a list, find the next item (if drag were to end now).
  var hoverNextItem =
      hoverList ? this.getHoverNextItem_(hoverList, draggerElCenter) : null;

  var rv = this.dispatchEvent(
      new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.BEFOREDRAGMOVE, this, dragEvent,
          this.currDragItem_, this.draggerEl_, this.dragger_, draggerElCenter,
          hoverList, hoverNextItem));
  if (!rv) {
    return false;
  }

  if (hoverList) {
    if (this.updateWhileDragging_) {
      this.insertCurrDragItem_(hoverList, hoverNextItem);
    } else {
      // If update while dragging is disabled do not insert
      // the dragged item, but update the hovered item instead.
      this.updateCurrHoverItem(hoverNextItem, draggerElCenter);
    }
    this.currDragItem_.style.display = '';
    // Add drag list's hover class (if any).
    if (hoverList.dlgDragHoverClass_) {
      goog.dom.classlist.add(
          goog.asserts.assert(hoverList), hoverList.dlgDragHoverClass_);
    }

  } else {
    // Not hovering over a drag list, so remove the item altogether unless
    // specified otherwise by the user.
    if (!this.isCurrDragItemAlwaysDisplayed_) {
      this.currDragItem_.style.display = 'none';
    }

    // Remove hover classes (if any) from all drag lists.
    for (var i = 0, n = this.dragLists_.length; i < n; i++) {
      var dragList = this.dragLists_[i];
      if (dragList.dlgDragHoverClass_) {
        goog.dom.classlist.remove(
            goog.asserts.assert(dragList), dragList.dlgDragHoverClass_);
      }
    }
  }

  // If the current hover list is different than the last, the lists may have
  // shrunk, so we should recache the bounds.
  if (hoverList != this.currHoverList_) {
    this.currHoverList_ = hoverList;
    this.recacheListAndItemBounds_(this.currDragItem_);
  }

  this.dispatchEvent(
      new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.DRAGMOVE, this, dragEvent,
          /** @type {Element} */ (this.currDragItem_), this.draggerEl_,
          this.dragger_, draggerElCenter, hoverList, hoverNextItem));

  // Return false to prevent selection due to mouse drag.
  return false;
};


/**
 * Clear all our temporary fields that are only defined while dragging, and
 * all the bounds info stored on the drag lists and drag elements.
 * @param {!goog.events.Event=} opt_e EARLY_CANCEL event from the dragger if
 *     cleanup_ was called as an event handler.
 * @private
 */
goog.fx.DragListGroup.prototype.cleanup_ = function(opt_e) {
  this.cleanupDragDom_();

  this.currDragItem_ = null;
  this.currHoverList_ = null;
  this.origList_ = null;
  this.origNextItem_ = null;
  this.draggerEl_ = null;
  this.dragger_ = null;

  // Note: IE doesn't allow 'delete' for fields on HTML elements (because
  // they're not real JS objects in IE), so we just set them to null.
  for (var i = 0, n = this.dragLists_.length; i < n; i++) {
    this.dragLists_[i].dlgBounds_ = null;
  }
  for (var i = 0, n = this.dragItems_.length; i < n; i++) {
    this.dragItems_[i].dlgBounds_ = null;
  }
};


/**
 * Handles the end or the cancellation of a drag action, i.e. END or CLEANUP
 * event fired by the dragger.
 *
 * @param {!goog.fx.DragEvent} dragEvent Event object fired by the dragger.
 * @return {boolean} Whether the event was handled.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragEnd_ = function(dragEvent) {
  var rv = this.dispatchEvent(
      new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.BEFOREDRAGEND, this, dragEvent,
          /** @type {Element} */ (this.currDragItem_), this.draggerEl_,
          this.dragger_));
  if (!rv) {
    return false;
  }

  // If update while dragging is disabled insert the current drag item into
  // its intended location.
  if (!this.updateWhileDragging_) {
    this.insertCurrHoverItem();
  }

  // The DRAGEND handler may need the new order of the list items. Clean up the
  // garbage.
  // TODO(user): Regression test.
  this.cleanupDragDom_();

  this.dispatchEvent(
      new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.DRAGEND, this, dragEvent,
          this.currDragItem_, this.draggerEl_, this.dragger_));

  this.cleanup_();

  return true;
};


/**
 * Cleans up DOM changes that are made by the {@code handleDrag*} methods.
 * @private
 */
goog.fx.DragListGroup.prototype.cleanupDragDom_ = function() {
  // Disposes of the dragger and remove the cloned drag item.
  goog.dispose(this.dragger_);
  if (this.draggerEl_) {
    goog.dom.removeNode(this.draggerEl_);
  }

  // If the current drag item is not in any list, put it back in its original
  // location.
  if (this.currDragItem_ && this.currDragItem_.style.display == 'none') {
    // Note: this.origNextItem_ may be null, but insertBefore() still works.
    this.origList_.insertBefore(this.currDragItem_, this.origNextItem_);
    this.currDragItem_.style.display = '';
  }

  // If there's a CSS class specified for the current drag item, remove it.
  // Otherwise, make the current drag item visible (instead of empty space).
  if (this.currDragItemClasses_ && this.currDragItem_) {
    goog.dom.classlist.removeAll(
        goog.asserts.assert(this.currDragItem_),
        this.currDragItemClasses_ || []);
  } else if (this.currDragItem_) {
    this.currDragItem_.style.visibility = '';
  }

  // Remove hover classes (if any) from all drag lists.
  for (var i = 0, n = this.dragLists_.length; i < n; i++) {
    var dragList = this.dragLists_[i];
    if (dragList.dlgDragHoverClass_) {
      goog.dom.classlist.remove(
          goog.asserts.assert(dragList), dragList.dlgDragHoverClass_);
    }
  }
};


/**
 * Default implementation of the function to get the "handle" element for a
 * drag item. By default, we use the whole drag item as the handle. Users can
 * change this by calling setFunctionToGetHandleForDragItem().
 *
 * @param {!Element} dragItem The drag item to get the handle for.
 * @return {Element} The dragItem element itself.
 * @private
 */
goog.fx.DragListGroup.prototype.getHandleForDragItem_ = function(dragItem) {
  return dragItem;
};


/**
 * Handles a MOUSEOVER event fired on a drag item.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragItemMouseover_ = function(e) {
  var targetEl = goog.asserts.assertElement(e.currentTarget);
  goog.dom.classlist.addAll(targetEl, this.dragItemHoverClasses_ || []);
};


/**
 * Handles a MOUSEOUT event fired on a drag item.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragItemMouseout_ = function(e) {
  var targetEl = goog.asserts.assertElement(e.currentTarget);
  goog.dom.classlist.removeAll(targetEl, this.dragItemHoverClasses_ || []);
};


/**
 * Handles a MOUSEOVER event fired on the handle element of a drag item.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragItemHandleMouseover_ = function(e) {
  var targetEl = goog.asserts.assertElement(e.currentTarget);
  goog.dom.classlist.addAll(targetEl, this.dragItemHandleHoverClasses_ || []);
};


/**
 * Handles a MOUSEOUT event fired on the handle element of a drag item.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.fx.DragListGroup.prototype.handleDragItemHandleMouseout_ = function(e) {
  var targetEl = goog.asserts.assertElement(e.currentTarget);
  goog.dom.classlist.removeAll(
      targetEl, this.dragItemHandleHoverClasses_ || []);
};


/**
 * Helper for handleDragMove_().
 * Given the position of the center of the dragger element, figures out whether
 * it's currently hovering over any of the drag lists.
 *
 * @param {goog.math.Coordinate} draggerElCenter The center position of the
 *     dragger element.
 * @return {Element} If currently hovering over a drag list, returns the drag
 *     list element. Else returns null.
 * @private
 */
goog.fx.DragListGroup.prototype.getHoverDragList_ = function(draggerElCenter) {

  // If the current drag item was in a list last time we did this, then check
  // that same list first.
  var prevHoverList = null;
  if (this.currDragItem_.style.display != 'none') {
    prevHoverList = /** @type {Element} */ (this.currDragItem_.parentNode);
    // Important: We can't use the cached bounds for this list because the
    // cached bounds are based on the case where the current drag item is not
    // in the list. Since the current drag item is known to be in this list, we
    // must recompute the list's bounds.
    var prevHoverListBounds = goog.style.getBounds(prevHoverList);
    if (this.isInRect_(draggerElCenter, prevHoverListBounds)) {
      return prevHoverList;
    }
  }

  for (var i = 0, n = this.dragLists_.length; i < n; i++) {
    var dragList = this.dragLists_[i];
    if (dragList == prevHoverList) {
      continue;
    }
    if (this.isInRect_(draggerElCenter, dragList.dlgBounds_)) {
      return dragList;
    }
  }

  return null;
};


/**
 * Checks whether a coordinate position resides inside a rectangle.
 * @param {goog.math.Coordinate} pos The coordinate position.
 * @param {goog.math.Rect} rect The rectangle.
 * @return {boolean} True if 'pos' is within the bounds of 'rect'.
 * @private
 */
goog.fx.DragListGroup.prototype.isInRect_ = function(pos, rect) {
  return pos.x > rect.left && pos.x < rect.left + rect.width &&
      pos.y > rect.top && pos.y < rect.top + rect.height;
};


/**
 * Updates the value of currHoverItem_.
 *
 * This method is used for insertion only when updateWhileDragging_ is false.
 * The below implementation is the basic one. This method can be extended by
 * a subclass to support changes to hovered item (eg: highlighting). Parametr
 * opt_draggerElCenter can be used for more sophisticated effects.
 *
 * @param {Element} hoverNextItem element of the list that is hovered over.
 * @param {goog.math.Coordinate=} opt_draggerElCenter current position of
 *     the dragged element.
 * @protected
 */
goog.fx.DragListGroup.prototype.updateCurrHoverItem = function(
    hoverNextItem, opt_draggerElCenter) {
  if (hoverNextItem) {
    this.currHoverItem_ = hoverNextItem;
  }
};


/**
 * Inserts the currently dragged item in its new place.
 *
 * This method is used for insertion only when updateWhileDragging_ is false
 * (otherwise there is no need for that). In the basic implementation
 * the element is inserted before the currently hovered over item (this can
 * be changed by overriding the method in subclasses).
 *
 * @protected
 */
goog.fx.DragListGroup.prototype.insertCurrHoverItem = function() {
  this.origList_.insertBefore(this.currDragItem_, this.currHoverItem_);
};


/**
 * Helper for handleDragMove_().
 * Given the position of the center of the dragger element, plus the drag list
 * that it's currently hovering over, figures out the next drag item in the
 * list that follows the current position of the dragger element. (I.e. if
 * the drag action ends right now, it would become the item after the current
 * drag item.)
 *
 * @param {Element} hoverList The drag list that we're hovering over.
 * @param {goog.math.Coordinate} draggerElCenter The center position of the
 *     dragger element.
 * @return {Element} Returns the earliest item in the hover list that belongs
 *     after the current position of the dragger element. If all items in the
 *     list should come before the current drag item, then returns null.
 * @private
 */
goog.fx.DragListGroup.prototype.getHoverNextItem_ = function(
    hoverList, draggerElCenter) {
  if (hoverList == null) {
    throw Error('getHoverNextItem_ called with null hoverList.');
  }

  // The definition of what it means for the draggerEl to be "before" a given
  // item in the hover drag list is not always the same. It changes based on
  // the growth direction of the hover drag list in question.
  /** @type {number} */
  var relevantCoord = 0;
  var getRelevantBoundFn;
  var isBeforeFn;
  var pickClosestRow = false;
  var distanceToClosestRow = undefined;
  switch (hoverList.dlgGrowthDirection_) {
    case goog.fx.DragListDirection.DOWN:
      // "Before" means draggerElCenter.y is less than item's bottom y-value.
      relevantCoord = draggerElCenter.y;
      getRelevantBoundFn = goog.fx.DragListGroup.getBottomBound_;
      isBeforeFn = goog.fx.DragListGroup.isLessThan_;
      break;
    case goog.fx.DragListDirection.RIGHT_2D:
      pickClosestRow = true;
    case goog.fx.DragListDirection.RIGHT:
      // "Before" means draggerElCenter.x is less than item's right x-value.
      relevantCoord = draggerElCenter.x;
      getRelevantBoundFn = goog.fx.DragListGroup.getRightBound_;
      isBeforeFn = goog.fx.DragListGroup.isLessThan_;
      break;
    case goog.fx.DragListDirection.LEFT_2D:
      pickClosestRow = true;
    case goog.fx.DragListDirection.LEFT:
      // "Before" means draggerElCenter.x is greater than item's left x-value.
      relevantCoord = draggerElCenter.x;
      getRelevantBoundFn = goog.fx.DragListGroup.getLeftBound_;
      isBeforeFn = goog.fx.DragListGroup.isGreaterThan_;
      break;
  }

  // This holds the earliest drag item found so far that should come after
  // this.currDragItem_ in the hover drag list (based on draggerElCenter).
  var earliestAfterItem = null;
  // This is the position of the relevant bound for the earliestAfterItem,
  // where "relevant" is determined by the growth direction of hoverList.
  var earliestAfterItemRelevantBound;

  var hoverListItems = goog.dom.getChildren(hoverList);
  for (var i = 0, n = hoverListItems.length; i < n; i++) {
    var item = hoverListItems[i];
    if (item == this.currDragItem_) {
      continue;
    }

    var relevantBound = getRelevantBoundFn(item.dlgBounds_);
    // When the hoverlist is broken into multiple rows (i.e., in the case of
    // LEFT_2D and RIGHT_2D) it is no longer enough to only look at the
    // x-coordinate alone in order to find the {@earliestAfterItem} in the
    // hoverlist. Make sure it is chosen from the row closest to the
    // {@code draggerElCenter}.
    if (pickClosestRow) {
      var distanceToRow = goog.fx.DragListGroup.verticalDistanceFromItem_(
          item, draggerElCenter);
      // Initialize the distance to the closest row to the current value if
      // undefined.
      if (!goog.isDef(distanceToClosestRow)) {
        distanceToClosestRow = distanceToRow;
      }
      if (isBeforeFn(relevantCoord, relevantBound) &&
          (earliestAfterItemRelevantBound == undefined ||
           (distanceToRow < distanceToClosestRow) ||
           ((distanceToRow == distanceToClosestRow) &&
            (isBeforeFn(relevantBound, earliestAfterItemRelevantBound) ||
             relevantBound == earliestAfterItemRelevantBound)))) {
        earliestAfterItem = item;
        earliestAfterItemRelevantBound = relevantBound;
      }
      // Update distance to closest row.
      if (distanceToRow < distanceToClosestRow) {
        distanceToClosestRow = distanceToRow;
      }
    } else if (
        isBeforeFn(relevantCoord, relevantBound) &&
        (earliestAfterItemRelevantBound == undefined ||
         isBeforeFn(relevantBound, earliestAfterItemRelevantBound))) {
      earliestAfterItem = item;
      earliestAfterItemRelevantBound = relevantBound;
    }
  }
  // If we ended up picking an element that is not in the closest row it can
  // only happen if we should have picked the last one in which case there is
  // no consecutive element.
  if (!goog.isNull(earliestAfterItem) &&
      goog.fx.DragListGroup.verticalDistanceFromItem_(
          earliestAfterItem, draggerElCenter) > distanceToClosestRow) {
    return null;
  } else {
    return earliestAfterItem;
  }
};


/**
 * Private helper for getHoverNextItem().
 * Given an item and a target determine the vertical distance from the item's
 * center to the target.
 * @param {Element} item The item to measure the distance from.
 * @param {goog.math.Coordinate} target The (x,y) coordinate of the target
 *     to measure the distance to.
 * @return {number} The vertical distance between the center of the item and
 *     the target.
 * @private
 */
goog.fx.DragListGroup.verticalDistanceFromItem_ = function(item, target) {
  var itemBounds = item.dlgBounds_;
  var itemCenterY = itemBounds.top + (itemBounds.height - 1) / 2;
  return Math.abs(target.y - itemCenterY);
};


/**
 * Private helper for getHoverNextItem_().
 * Given the bounds of an item, computes the item's bottom y-value.
 * @param {goog.math.Rect} itemBounds The bounds of the item.
 * @return {number} The item's bottom y-value.
 * @private
 */
goog.fx.DragListGroup.getBottomBound_ = function(itemBounds) {
  return itemBounds.top + itemBounds.height - 1;
};


/**
 * Private helper for getHoverNextItem_().
 * Given the bounds of an item, computes the item's right x-value.
 * @param {goog.math.Rect} itemBounds The bounds of the item.
 * @return {number} The item's right x-value.
 * @private
 */
goog.fx.DragListGroup.getRightBound_ = function(itemBounds) {
  return itemBounds.left + itemBounds.width - 1;
};


/**
 * Private helper for getHoverNextItem_().
 * Given the bounds of an item, computes the item's left x-value.
 * @param {goog.math.Rect} itemBounds The bounds of the item.
 * @return {number} The item's left x-value.
 * @private
 */
goog.fx.DragListGroup.getLeftBound_ = function(itemBounds) {
  return itemBounds.left || 0;
};


/**
 * Private helper for getHoverNextItem_().
 * @param {number} a Number to compare.
 * @param {number} b Number to compare.
 * @return {boolean} Whether a is less than b.
 * @private
 */
goog.fx.DragListGroup.isLessThan_ = function(a, b) {
  return a < b;
};


/**
 * Private helper for getHoverNextItem_().
 * @param {number} a Number to compare.
 * @param {number} b Number to compare.
 * @return {boolean} Whether a is greater than b.
 * @private
 */
goog.fx.DragListGroup.isGreaterThan_ = function(a, b) {
  return a > b;
};


/**
 * Inserts the current drag item to the appropriate location in the drag list
 * that we're hovering over (if the current drag item is not already there).
 *
 * @param {Element} hoverList The drag list we're hovering over.
 * @param {Element} hoverNextItem The next item in the hover drag list.
 * @private
 */
goog.fx.DragListGroup.prototype.insertCurrDragItem_ = function(
    hoverList, hoverNextItem) {
  if (this.currDragItem_.parentNode != hoverList ||
      goog.dom.getNextElementSibling(this.currDragItem_) != hoverNextItem) {
    // The current drag item is not in the correct location, so we move it.
    // Note: hoverNextItem may be null, but insertBefore() still works.
    hoverList.insertBefore(this.currDragItem_, hoverNextItem);
  }
};



/**
 * The event object dispatched by DragListGroup.
 * The fields draggerElCenter, hoverList, and hoverNextItem are only available
 * for the BEFOREDRAGMOVE and DRAGMOVE events.
 *
 * @param {goog.fx.DragListGroup.EventType} type
 * @param {goog.fx.DragListGroup} dragListGroup A reference to the associated
 *     DragListGroup object.
 * @param {goog.events.BrowserEvent|goog.fx.DragEvent} event The event fired
 *     by the browser or fired by the dragger.
 * @param {Element} currDragItem The current drag item being moved.
 * @param {Element} draggerEl The clone of the current drag item that's actually
 *     being dragged around.
 * @param {goog.fx.Dragger} dragger The dragger object.
 * @param {goog.math.Coordinate=} opt_draggerElCenter The current center
 *     position of the draggerEl.
 * @param {Element=} opt_hoverList The current drag list that's being hovered
 *     over, or null if the center of draggerEl is outside of any drag lists.
 *     If not null and the drag action ends right now, then currDragItem will
 *     end up in this list.
 * @param {Element=} opt_hoverNextItem The current next item in the hoverList
 *     that the draggerEl is hovering over. (I.e. If the drag action ends
 *     right now, then this item would become the next item after the new
 *     location of currDragItem.) May be null if not applicable or if
 *     currDragItem would be added to the end of hoverList.
 * @constructor
 * @struct
 * @extends {goog.events.Event}
 */
goog.fx.DragListGroupEvent = function(
    type, dragListGroup, event, currDragItem, draggerEl, dragger,
    opt_draggerElCenter, opt_hoverList, opt_hoverNextItem) {
  goog.events.Event.call(this, type);

  /**
   * A reference to the associated DragListGroup object.
   * @type {goog.fx.DragListGroup}
   */
  this.dragListGroup = dragListGroup;

  /**
   * The event fired by the browser or fired by the dragger.
   * @type {goog.events.BrowserEvent|goog.fx.DragEvent}
   */
  this.event = event;

  /**
   * The current drag item being move.
   * @type {Element}
   */
  this.currDragItem = currDragItem;

  /**
   * The clone of the current drag item that's actually being dragged around.
   * @type {Element}
   */
  this.draggerEl = draggerEl;

  /**
   * The dragger object.
   * @type {goog.fx.Dragger}
   */
  this.dragger = dragger;

  /**
   * The current center position of the draggerEl.
   * @type {goog.math.Coordinate|undefined}
   */
  this.draggerElCenter = opt_draggerElCenter;

  /**
   * The current drag list that's being hovered over, or null if the center of
   * draggerEl is outside of any drag lists. (I.e. If not null and the drag
   * action ends right now, then currDragItem will end up in this list.)
   * @type {Element|undefined}
   */
  this.hoverList = opt_hoverList;

  /**
   * The current next item in the hoverList that the draggerEl is hovering over.
   * (I.e. If the drag action ends right now, then this item would become the
   * next item after the new location of currDragItem.) May be null if not
   * applicable or if currDragItem would be added to the end of hoverList.
   * @type {Element|undefined}
   */
  this.hoverNextItem = opt_hoverNextItem;
};
goog.inherits(goog.fx.DragListGroupEvent, goog.events.Event);
