text: // 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 = {
    /**
     * Raised on mouse down, when the dragger is first created.  Handle this event
     * to customize the dragger element even if the drag never actually starts (if
     * the mouse never moves beyond hysteresis).
     */
    DRAGGERCREATED:
        new goog.events.EventId(goog.events.getUniqueId('draggercreated')),
    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'),
    /** Raised after the dragged item is moved to the new spot. */
    DRAGEND: new goog.events.EventId('dragend'),
    /**
     * Raised whenever the dragger element is removed:
     *  - When a drag completes successfully.
     *  - If the drag never started due to mouseup within hysteresis.
     *  - If the drag was cancelled by a BEFORE* event.
     *  - If the drag was cancelled due to focus loss.
     */
    DRAGGERREMOVED:
        new goog.events.EventId(goog.events.getUniqueId('draggerremoved'))
  };
  
  
  /**
   * 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.dispatchEvent(new goog.fx.DragListGroupEvent(
        goog.fx.DragListGroup.EventType.DRAGGERCREATED, this, e,
        this.currDragItem_, this.draggerEl_, this.dragger_));
    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_);
    var hadDragger = this.draggerEl_ && this.draggerEl_.parentElement;
    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_);
      }
    }
    if (hadDragger) {
      this.dispatchEvent(new goog.fx.DragListGroupEvent(
          goog.fx.DragListGroup.EventType.DRAGGERREMOVED, this, null,
          this.currDragItem_, this.draggerEl_, this.dragger_));
    }
  };
  
  
  /**
   * 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 new 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);
  