blob: dc20ec02a8b4ed33ffce552af5063305bb5a1e40 [file] [log] [blame]
// 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.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
*/
goog.fx.DragListGroup = function() {
goog.events.EventTarget.call(this);
/**
* The drag lists.
* @type {Array<Element>}
* @private
*/
this.dragLists_ = [];
/**
* All the drag items. Set by init().
* @type {Array<Element>}
* @private
*/
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.
* @type {Object}
* @private
*/
this.dragItemForHandle_ = {};
/**
* The event handler for this instance.
* @type {goog.events.EventHandler<!goog.fx.DragListGroup>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Whether the setup has been done to make all items in all lists draggable.
* @type {boolean}
* @private
*/
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.
* @type {boolean}
* @private
*/
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.
* @type {boolean}
* @private
*/
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.
* @const
*/
goog.fx.DragListGroup.EventType = {
BEFOREDRAGSTART: 'beforedragstart',
DRAGSTART: 'dragstart',
BEFOREDRAGMOVE: 'beforedragmove',
DRAGMOVE: 'dragmove',
BEFOREDRAGEND: 'beforedragend',
DRAGEND: 'dragend'
};
// The next 4 are user-supplied CSS classes.
/**
* The user-supplied CSS classes to add to a drag item on hover (not during a
* drag action).
* @type {Array|undefined}
* @private
*/
goog.fx.DragListGroup.prototype.dragItemHoverClasses_;
/**
* The user-supplied CSS classes to add to a drag item handle on hover (not
* during a drag action).
* @type {Array|undefined}
* @private
*/
goog.fx.DragListGroup.prototype.dragItemHandleHoverClasses_;
/**
* The user-supplied CSS classes to add to the current drag item (during a drag
* action).
* @type {Array|undefined}
* @private
*/
goog.fx.DragListGroup.prototype.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).
* @type {Array<string>|undefined}
* @private
*/
goog.fx.DragListGroup.prototype.draggerElClasses_;
// The next 5 are info applicable during a drag action.
/**
* The current drag item being moved.
* Note: This is only defined while a drag action is happening.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.currDragItem_;
/**
* The drag list that {@code this.currDragItem_} is currently hovering over, or
* null if it is not hovering over a list.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.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.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.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.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.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.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.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.
* @type {Element}
* @private
*/
goog.fx.DragListGroup.prototype.draggerEl_;
/**
* The dragger object.
* Note: This is only defined while a drag action is happening.
* @type {goog.fx.Dragger}
* @private
*/
goog.fx.DragListGroup.prototype.dragger_;
/**
* The amount of distance, in pixels, after which a mousedown or touchstart is
* considered a drag.
* @type {number}
* @private
*/
goog.fx.DragListGroup.prototype.hysteresisDistance_ = 0;
/**
* 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_;
};
/**
* 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.
*
* @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_ = 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 = 'visible';
}
// 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;
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 {string} type The event type string.
* @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
* @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);