blob: afd4af8c9fde60cf1be49449eb4ecb7c6f40aa77 [file] [log] [blame]
// Copyright 2006 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 Abstract Base Class for Drag and Drop.
*
* Provides functionality for implementing drag and drop classes. Also provides
* support classes and events.
*
* @author eae@google.com (Emil A Eklund)
*/
goog.provide('goog.fx.AbstractDragDrop');
goog.provide('goog.fx.AbstractDragDrop.EventType');
goog.provide('goog.fx.DragDropEvent');
goog.provide('goog.fx.DragDropItem');
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.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
/**
* Abstract class that provides reusable functionality for implementing drag
* and drop functionality.
*
* This class also allows clients to define their own subtargeting function
* so that drop areas can have finer granularity then a singe element. This is
* accomplished by using a client provided function to map from element and
* coordinates to a subregion id.
*
* This class can also be made aware of scrollable containers that contain
* drop targets by calling addScrollableContainer. This will cause dnd to
* take changing scroll positions into account while a drag is occuring.
*
* @extends {goog.events.EventTarget}
* @constructor
*/
goog.fx.AbstractDragDrop = function() {
goog.fx.AbstractDragDrop.base(this, 'constructor');
/**
* List of items that makes up the drag source or drop target.
* @type {Array<goog.fx.DragDropItem>}
* @protected
* @suppress {underscore|visibility}
*/
this.items_ = [];
/**
* List of associated drop targets.
* @type {Array<goog.fx.AbstractDragDrop>}
* @private
*/
this.targets_ = [];
/**
* Scrollable containers to account for during drag
* @type {Array<goog.fx.ScrollableContainer_>}
* @private
*/
this.scrollableContainers_ = [];
};
goog.inherits(goog.fx.AbstractDragDrop, goog.events.EventTarget);
/**
* Minimum size (in pixels) for a dummy target. If the box for the target is
* less than the specified size it's not created.
* @type {number}
* @private
*/
goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ = 10;
/**
* Flag indicating if it's a drag source, set by addTarget.
* @type {boolean}
* @private
*/
goog.fx.AbstractDragDrop.prototype.isSource_ = false;
/**
* Flag indicating if it's a drop target, set when added as target to another
* DragDrop object.
* @type {boolean}
* @private
*/
goog.fx.AbstractDragDrop.prototype.isTarget_ = false;
/**
* Subtargeting function accepting args:
* (goog.fx.DragDropItem, goog.math.Box, number, number)
* @type {Function}
* @private
*/
goog.fx.AbstractDragDrop.prototype.subtargetFunction_;
/**
* Last active subtarget.
* @type {Object}
* @private
*/
goog.fx.AbstractDragDrop.prototype.activeSubtarget_;
/**
* Class name to add to source elements being dragged. Set by setDragClass.
* @type {?string}
* @private
*/
goog.fx.AbstractDragDrop.prototype.dragClass_;
/**
* Class name to add to source elements. Set by setSourceClass.
* @type {?string}
* @private
*/
goog.fx.AbstractDragDrop.prototype.sourceClass_;
/**
* Class name to add to target elements. Set by setTargetClass.
* @type {?string}
* @private
*/
goog.fx.AbstractDragDrop.prototype.targetClass_;
/**
* The SCROLL event target used to make drag element follow scrolling.
* @type {EventTarget}
* @private
*/
goog.fx.AbstractDragDrop.prototype.scrollTarget_;
/**
* Dummy target, {@see maybeCreateDummyTargetForPosition_}.
* @type {goog.fx.ActiveDropTarget_}
* @private
*/
goog.fx.AbstractDragDrop.prototype.dummyTarget_;
/**
* Whether the object has been initialized.
* @type {boolean}
* @private
*/
goog.fx.AbstractDragDrop.prototype.initialized_ = false;
/**
* Constants for event names
* @const
*/
goog.fx.AbstractDragDrop.EventType = {
DRAGOVER: 'dragover',
DRAGOUT: 'dragout',
DRAG: 'drag',
DROP: 'drop',
DRAGSTART: 'dragstart',
DRAGEND: 'dragend'
};
/**
* Constant for distance threshold, in pixels, an element has to be moved to
* initiate a drag operation.
* @type {number}
*/
goog.fx.AbstractDragDrop.initDragDistanceThreshold = 5;
/**
* Set class to add to source elements being dragged.
*
* @param {string} className Class to be added. Must be a single, valid
* classname.
*/
goog.fx.AbstractDragDrop.prototype.setDragClass = function(className) {
this.dragClass_ = className;
};
/**
* Set class to add to source elements.
*
* @param {string} className Class to be added. Must be a single, valid
* classname.
*/
goog.fx.AbstractDragDrop.prototype.setSourceClass = function(className) {
this.sourceClass_ = className;
};
/**
* Set class to add to target elements.
*
* @param {string} className Class to be added. Must be a single, valid
* classname.
*/
goog.fx.AbstractDragDrop.prototype.setTargetClass = function(className) {
this.targetClass_ = className;
};
/**
* Whether the control has been initialized.
*
* @return {boolean} True if it's been initialized.
*/
goog.fx.AbstractDragDrop.prototype.isInitialized = function() {
return this.initialized_;
};
/**
* Add item to drag object.
*
* @param {Element|string} element Dom Node, or string representation of node
* id, to be used as drag source/drop target.
* @throws Error Thrown if called on instance of abstract class
*/
goog.fx.AbstractDragDrop.prototype.addItem = goog.abstractMethod;
/**
* Associate drop target with drag element.
*
* @param {goog.fx.AbstractDragDrop} target Target to add.
*/
goog.fx.AbstractDragDrop.prototype.addTarget = function(target) {
this.targets_.push(target);
target.isTarget_ = true;
this.isSource_ = true;
};
/**
* Sets the SCROLL event target to make drag element follow scrolling.
*
* @param {EventTarget} scrollTarget The element that dispatches SCROLL events.
*/
goog.fx.AbstractDragDrop.prototype.setScrollTarget = function(scrollTarget) {
this.scrollTarget_ = scrollTarget;
};
/**
* Initialize drag and drop functionality for sources/targets already added.
* Sources/targets added after init has been called will initialize themselves
* one by one.
*/
goog.fx.AbstractDragDrop.prototype.init = function() {
if (this.initialized_) {
return;
}
for (var item, i = 0; item = this.items_[i]; i++) {
this.initItem(item);
}
this.initialized_ = true;
};
/**
* Initializes a single item.
*
* @param {goog.fx.DragDropItem} item Item to initialize.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.initItem = function(item) {
if (this.isSource_) {
goog.events.listen(item.element, goog.events.EventType.MOUSEDOWN,
item.mouseDown_, false, item);
if (this.sourceClass_) {
goog.dom.classlist.add(
goog.asserts.assert(item.element), this.sourceClass_);
}
}
if (this.isTarget_ && this.targetClass_) {
goog.dom.classlist.add(
goog.asserts.assert(item.element), this.targetClass_);
}
};
/**
* Called when removing an item. Removes event listeners and classes.
*
* @param {goog.fx.DragDropItem} item Item to dispose.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.disposeItem = function(item) {
if (this.isSource_) {
goog.events.unlisten(item.element, goog.events.EventType.MOUSEDOWN,
item.mouseDown_, false, item);
if (this.sourceClass_) {
goog.dom.classlist.remove(
goog.asserts.assert(item.element), this.sourceClass_);
}
}
if (this.isTarget_ && this.targetClass_) {
goog.dom.classlist.remove(
goog.asserts.assert(item.element), this.targetClass_);
}
item.dispose();
};
/**
* Removes all items.
*/
goog.fx.AbstractDragDrop.prototype.removeItems = function() {
for (var item, i = 0; item = this.items_[i]; i++) {
this.disposeItem(item);
}
this.items_.length = 0;
};
/**
* Starts a drag event for an item if the mouse button stays pressed and the
* cursor moves a few pixels. Allows dragging of items without first having to
* register them with addItem.
*
* @param {goog.events.BrowserEvent} event Mouse down event.
* @param {goog.fx.DragDropItem} item Item that's being dragged.
*/
goog.fx.AbstractDragDrop.prototype.maybeStartDrag = function(event, item) {
item.maybeStartDrag_(event, item.element);
};
/**
* Event handler that's used to start drag.
*
* @param {goog.events.BrowserEvent} event Mouse move event.
* @param {goog.fx.DragDropItem} item Item that's being dragged.
*/
goog.fx.AbstractDragDrop.prototype.startDrag = function(event, item) {
// Prevent a new drag operation from being started if another one is already
// in progress (could happen if the mouse was released outside of the
// document).
if (this.dragItem_) {
return;
}
this.dragItem_ = item;
// Dispatch DRAGSTART event
var dragStartEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGSTART, this, this.dragItem_);
if (this.dispatchEvent(dragStartEvent) == false) {
this.dragItem_ = null;
return;
}
// Get the source element and create a drag element for it.
var el = item.getCurrentDragElement();
this.dragEl_ = this.createDragElement(el);
var doc = goog.dom.getOwnerDocument(el);
doc.body.appendChild(this.dragEl_);
this.dragger_ = this.createDraggerFor(el, this.dragEl_, event);
this.dragger_.setScrollTarget(this.scrollTarget_);
goog.events.listen(this.dragger_, goog.fx.Dragger.EventType.DRAG,
this.moveDrag_, false, this);
goog.events.listen(this.dragger_, goog.fx.Dragger.EventType.END,
this.endDrag, false, this);
// IE may issue a 'selectstart' event when dragging over an iframe even when
// default mousemove behavior is suppressed. If the default selectstart
// behavior is not suppressed, elements dragged over will show as selected.
goog.events.listen(doc.body, goog.events.EventType.SELECTSTART,
this.suppressSelect_);
this.recalculateDragTargets();
this.recalculateScrollableContainers();
this.activeTarget_ = null;
this.initScrollableContainerListeners_();
this.dragger_.startDrag(event);
event.preventDefault();
};
/**
* Recalculates the geometry of this source's drag targets. Call this
* if the position or visibility of a drag target has changed during
* a drag, or if targets are added or removed.
*
* TODO(user): this is an expensive operation; more efficient APIs
* may be necessary.
*/
goog.fx.AbstractDragDrop.prototype.recalculateDragTargets = function() {
this.targetList_ = [];
for (var target, i = 0; target = this.targets_[i]; i++) {
for (var itm, j = 0; itm = target.items_[j]; j++) {
this.addDragTarget_(target, itm);
}
}
if (!this.targetBox_) {
this.targetBox_ = new goog.math.Box(0, 0, 0, 0);
}
};
/**
* Recalculates the current scroll positions of scrollable containers and
* allocates targets. Call this if the position of a container changed or if
* targets are added or removed.
*/
goog.fx.AbstractDragDrop.prototype.recalculateScrollableContainers =
function() {
var container, i, j, target;
for (i = 0; container = this.scrollableContainers_[i]; i++) {
container.containedTargets_ = [];
container.savedScrollLeft_ = container.element_.scrollLeft;
container.savedScrollTop_ = container.element_.scrollTop;
var pos = goog.style.getPageOffset(container.element_);
var size = goog.style.getSize(container.element_);
container.box_ = new goog.math.Box(pos.y, pos.x + size.width,
pos.y + size.height, pos.x);
}
for (i = 0; target = this.targetList_[i]; i++) {
for (j = 0; container = this.scrollableContainers_[j]; j++) {
if (goog.dom.contains(container.element_, target.element_)) {
container.containedTargets_.push(target);
target.scrollableContainer_ = container;
}
}
}
};
/**
* Creates the Dragger for the drag element.
* @param {Element} sourceEl Drag source element.
* @param {Element} el the element created by createDragElement().
* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.
* @return {!goog.fx.Dragger} The new Dragger.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.createDraggerFor =
function(sourceEl, el, event) {
// Position the drag element.
var pos = this.getDragElementPosition(sourceEl, el, event);
el.style.position = 'absolute';
el.style.left = pos.x + 'px';
el.style.top = pos.y + 'px';
return new goog.fx.Dragger(el);
};
/**
* Event handler that's used to stop drag. Fires a drop event if over a valid
* target.
*
* @param {goog.fx.DragEvent} event Drag event.
*/
goog.fx.AbstractDragDrop.prototype.endDrag = function(event) {
var activeTarget = event.dragCanceled ? null : this.activeTarget_;
if (activeTarget && activeTarget.target_) {
var clientX = event.clientX;
var clientY = event.clientY;
var scroll = this.getScrollPos();
var x = clientX + scroll.x;
var y = clientY + scroll.y;
var subtarget;
// If a subtargeting function is enabled get the current subtarget
if (this.subtargetFunction_) {
subtarget = this.subtargetFunction_(activeTarget.item_,
activeTarget.box_, x, y);
}
var dragEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,
activeTarget.target_, activeTarget.item_, activeTarget.element_,
clientX, clientY, x, y);
this.dispatchEvent(dragEvent);
var dropEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DROP, this, this.dragItem_,
activeTarget.target_, activeTarget.item_, activeTarget.element_,
clientX, clientY, x, y, subtarget);
activeTarget.target_.dispatchEvent(dropEvent);
}
var dragEndEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGEND, this, this.dragItem_);
this.dispatchEvent(dragEndEvent);
goog.events.unlisten(this.dragger_, goog.fx.Dragger.EventType.DRAG,
this.moveDrag_, false, this);
goog.events.unlisten(this.dragger_, goog.fx.Dragger.EventType.END,
this.endDrag, false, this);
var doc = goog.dom.getOwnerDocument(this.dragItem_.getCurrentDragElement());
goog.events.unlisten(doc.body, goog.events.EventType.SELECTSTART,
this.suppressSelect_);
this.afterEndDrag(this.activeTarget_ ? this.activeTarget_.item_ : null);
};
/**
* Called after a drag operation has finished.
*
* @param {goog.fx.DragDropItem=} opt_dropTarget Target for successful drop.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.afterEndDrag = function(opt_dropTarget) {
this.disposeDrag();
};
/**
* Called once a drag operation has finished. Removes event listeners and
* elements.
*
* @protected
*/
goog.fx.AbstractDragDrop.prototype.disposeDrag = function() {
this.disposeScrollableContainerListeners_();
this.dragger_.dispose();
goog.dom.removeNode(this.dragEl_);
delete this.dragItem_;
delete this.dragEl_;
delete this.dragger_;
delete this.targetList_;
delete this.activeTarget_;
};
/**
* Event handler for drag events. Determines the active drop target, if any, and
* fires dragover and dragout events appropriately.
*
* @param {goog.fx.DragEvent} event Drag event.
* @private
*/
goog.fx.AbstractDragDrop.prototype.moveDrag_ = function(event) {
var position = this.getEventPosition(event);
var x = position.x;
var y = position.y;
// Check if we're still inside the bounds of the active target, if not fire
// a dragout event and proceed to find a new target.
var activeTarget = this.activeTarget_;
var subtarget;
if (activeTarget) {
// If a subtargeting function is enabled get the current subtarget
if (this.subtargetFunction_ && activeTarget.target_) {
subtarget = this.subtargetFunction_(activeTarget.item_,
activeTarget.box_, x, y);
}
if (activeTarget.box_.contains(position) &&
subtarget == this.activeSubtarget_) {
return;
}
if (activeTarget.target_) {
var sourceDragOutEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGOUT, this, this.dragItem_,
activeTarget.target_, activeTarget.item_, activeTarget.element_);
this.dispatchEvent(sourceDragOutEvent);
// The event should be dispatched the by target DragDrop so that the
// target DragDrop can manage these events without having to know what
// sources this is a target for.
var targetDragOutEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGOUT,
this,
this.dragItem_,
activeTarget.target_,
activeTarget.item_,
activeTarget.element_,
undefined,
undefined,
undefined,
undefined,
this.activeSubtarget_);
activeTarget.target_.dispatchEvent(targetDragOutEvent);
}
this.activeSubtarget_ = subtarget;
this.activeTarget_ = null;
}
// Check if inside target box
if (this.targetBox_.contains(position)) {
// Search for target and fire a dragover event if found
activeTarget = this.activeTarget_ = this.getTargetFromPosition_(position);
if (activeTarget && activeTarget.target_) {
// If a subtargeting function is enabled get the current subtarget
if (this.subtargetFunction_) {
subtarget = this.subtargetFunction_(activeTarget.item_,
activeTarget.box_, x, y);
}
var sourceDragOverEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,
activeTarget.target_, activeTarget.item_, activeTarget.element_);
sourceDragOverEvent.subtarget = subtarget;
this.dispatchEvent(sourceDragOverEvent);
// The event should be dispatched by the target DragDrop so that the
// target DragDrop can manage these events without having to know what
// sources this is a target for.
var targetDragOverEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,
activeTarget.target_, activeTarget.item_, activeTarget.element_,
event.clientX, event.clientY, undefined, undefined, subtarget);
activeTarget.target_.dispatchEvent(targetDragOverEvent);
} else if (!activeTarget) {
// If no target was found create a dummy one so we won't have to iterate
// over all possible targets for every move event.
this.activeTarget_ = this.maybeCreateDummyTargetForPosition_(x, y);
}
}
};
/**
* Event handler for suppressing selectstart events. Selecting should be
* disabled while dragging.
*
* @param {goog.events.Event} event The selectstart event to suppress.
* @return {boolean} Whether to perform default behavior.
* @private
*/
goog.fx.AbstractDragDrop.prototype.suppressSelect_ = function(event) {
return false;
};
/**
* Sets up listeners for the scrollable containers that keep track of their
* scroll positions.
* @private
*/
goog.fx.AbstractDragDrop.prototype.initScrollableContainerListeners_ =
function() {
var container, i;
for (i = 0; container = this.scrollableContainers_[i]; i++) {
goog.events.listen(container.element_, goog.events.EventType.SCROLL,
this.containerScrollHandler_, false, this);
}
};
/**
* Cleans up the scrollable container listeners.
* @private
*/
goog.fx.AbstractDragDrop.prototype.disposeScrollableContainerListeners_ =
function() {
for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {
goog.events.unlisten(container.element_, 'scroll',
this.containerScrollHandler_, false, this);
container.containedTargets_ = [];
}
};
/**
* Makes drag and drop aware of a target container that could scroll mid drag.
* @param {Element} element The scroll container.
*/
goog.fx.AbstractDragDrop.prototype.addScrollableContainer = function(element) {
this.scrollableContainers_.push(new goog.fx.ScrollableContainer_(element));
};
/**
* Removes all scrollable containers.
*/
goog.fx.AbstractDragDrop.prototype.removeAllScrollableContainers = function() {
this.disposeScrollableContainerListeners_();
this.scrollableContainers_ = [];
};
/**
* Event handler for containers scrolling.
* @param {goog.events.Event} e The event.
* @private
*/
goog.fx.AbstractDragDrop.prototype.containerScrollHandler_ = function(e) {
for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {
if (e.target == container.element_) {
var deltaTop = container.savedScrollTop_ - container.element_.scrollTop;
var deltaLeft =
container.savedScrollLeft_ - container.element_.scrollLeft;
container.savedScrollTop_ = container.element_.scrollTop;
container.savedScrollLeft_ = container.element_.scrollLeft;
// When the container scrolls, it's possible that one of the targets will
// move to the region contained by the dummy target. Since we don't know
// which sides (if any) of the dummy target are defined by targets
// contained by this container, we are conservative and just shrink it.
if (this.dummyTarget_ && this.activeTarget_ == this.dummyTarget_) {
if (deltaTop > 0) {
this.dummyTarget_.box_.top += deltaTop;
} else {
this.dummyTarget_.box_.bottom += deltaTop;
}
if (deltaLeft > 0) {
this.dummyTarget_.box_.left += deltaLeft;
} else {
this.dummyTarget_.box_.right += deltaLeft;
}
}
for (var j = 0, target; target = container.containedTargets_[j]; j++) {
var box = target.box_;
box.top += deltaTop;
box.left += deltaLeft;
box.bottom += deltaTop;
box.right += deltaLeft;
this.calculateTargetBox_(box);
}
}
}
this.dragger_.onScroll_(e);
};
/**
* Set a function that provides subtargets. A subtargeting function
* returns an arbitrary identifier for each subtarget of an element.
* DnD code will generate additional drag over / out events when
* switching from subtarget to subtarget. This is useful for instance
* if you are interested if you are on the top half or the bottom half
* of the element.
* The provided function will be given the DragDropItem, box, x, y
* box is the current window coordinates occupied by element
* x, y is the mouse position in window coordinates
*
* @param {Function} f The new subtarget function.
*/
goog.fx.AbstractDragDrop.prototype.setSubtargetFunction = function(f) {
this.subtargetFunction_ = f;
};
/**
* Creates an element for the item being dragged.
*
* @param {Element} sourceEl Drag source element.
* @return {Element} The new drag element.
*/
goog.fx.AbstractDragDrop.prototype.createDragElement = function(sourceEl) {
var dragEl = this.createDragElementInternal(sourceEl);
goog.asserts.assert(dragEl);
if (this.dragClass_) {
goog.dom.classlist.add(dragEl, this.dragClass_);
}
return dragEl;
};
/**
* Returns the position for the drag element.
*
* @param {Element} el Drag source element.
* @param {Element} dragEl The dragged element created by createDragElement().
* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.
* @return {!goog.math.Coordinate} The position for the drag element.
*/
goog.fx.AbstractDragDrop.prototype.getDragElementPosition =
function(el, dragEl, event) {
var pos = goog.style.getPageOffset(el);
// Subtract margin from drag element position twice, once to adjust the
// position given by the original node and once for the drag node.
var marginBox = goog.style.getMarginBox(el);
pos.x -= (marginBox.left || 0) * 2;
pos.y -= (marginBox.top || 0) * 2;
return pos;
};
/**
* Returns the dragger object.
*
* @return {goog.fx.Dragger} The dragger object used by this drag and drop
* instance.
*/
goog.fx.AbstractDragDrop.prototype.getDragger = function() {
return this.dragger_;
};
/**
* 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.AbstractDragDrop.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.AbstractDragDrop.prototype.createDragElementInternal =
function(sourceEl) {
return this.cloneNode_(sourceEl);
};
/**
* Add possible drop target for current drag operation.
*
* @param {goog.fx.AbstractDragDrop} target Drag handler.
* @param {goog.fx.DragDropItem} item Item that's being dragged.
* @private
*/
goog.fx.AbstractDragDrop.prototype.addDragTarget_ = function(target, item) {
// Get all the draggable elements and add each one.
var draggableElements = item.getDraggableElements();
var targetList = this.targetList_;
for (var i = 0; i < draggableElements.length; i++) {
var draggableElement = draggableElements[i];
// Determine target position and dimension
var box = this.getElementBox(item, draggableElement);
targetList.push(
new goog.fx.ActiveDropTarget_(box, target, item, draggableElement));
this.calculateTargetBox_(box);
}
};
/**
* Calculates the position and dimension of a draggable element.
*
* @param {goog.fx.DragDropItem} item Item that's being dragged.
* @param {Element} element The element to calculate the box.
*
* @return {!goog.math.Box} Box describing the position and dimension
* of element.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.getElementBox = function(item, element) {
var pos = goog.style.getPageOffset(element);
var size = goog.style.getSize(element);
return new goog.math.Box(pos.y, pos.x + size.width, pos.y + size.height,
pos.x);
};
/**
* Calculate the outer bounds (the region all targets are inside).
*
* @param {goog.math.Box} box Box describing the position and dimension
* of a drag target.
* @private
*/
goog.fx.AbstractDragDrop.prototype.calculateTargetBox_ = function(box) {
if (this.targetList_.length == 1) {
this.targetBox_ = new goog.math.Box(box.top, box.right,
box.bottom, box.left);
} else {
var tb = this.targetBox_;
tb.left = Math.min(box.left, tb.left);
tb.right = Math.max(box.right, tb.right);
tb.top = Math.min(box.top, tb.top);
tb.bottom = Math.max(box.bottom, tb.bottom);
}
};
/**
* Creates a dummy target for the given cursor position. The assumption is to
* create as big dummy target box as possible, the only constraints are:
* - The dummy target box cannot overlap any of real target boxes.
* - The dummy target has to contain a point with current mouse coordinates.
*
* NOTE: For performance reasons the box construction algorithm is kept simple
* and it is not optimal (see example below). Currently it is O(n) in regard to
* the number of real drop target boxes, but its result depends on the order
* of those boxes being processed (the order in which they're added to the
* targetList_ collection).
*
* The algorithm.
* a) Assumptions
* - Mouse pointer is in the bounding box of real target boxes.
* - None of the boxes have negative coordinate values.
* - Mouse pointer is not contained by any of "real target" boxes.
* - For targets inside a scrollable container, the box used is the
* intersection of the scrollable container's box and the target's box.
* This is because the part of the target that extends outside the scrollable
* container should not be used in the clipping calculations.
*
* b) Outline
* - Initialize the fake target to the bounding box of real targets.
* - For each real target box - clip the fake target box so it does not contain
* that target box, but does contain the mouse pointer.
* -- Project the real target box, mouse pointer and fake target box onto
* both axes and calculate the clipping coordinates.
* -- Only one coordinate is used to clip the fake target box to keep the
* fake target as big as possible.
* -- If the projection of the real target box contains the mouse pointer,
* clipping for a given axis is not possible.
* -- If both clippings are possible, the clipping more distant from the
* mouse pointer is selected to keep bigger fake target area.
* - Save the created fake target only if it has a big enough area.
*
*
* c) Example
* <pre>
* Input: Algorithm created box: Maximum box:
* +---------------------+ +---------------------+ +---------------------+
* | B1 | B2 | | B1 B2 | | B1 B2 |
* | | | | +-------------+ | |+-------------------+|
* |---------x-----------| | | | | || ||
* | | | | | | | || ||
* | | | | | | | || ||
* | | | | | | | || ||
* | | | | | | | || ||
* | | | | +-------------+ | |+-------------------+|
* | B4 | B3 | | B4 B3 | | B4 B3 |
* +---------------------+ +---------------------+ +---------------------+
* </pre>
*
* @param {number} x Cursor position on the x-axis.
* @param {number} y Cursor position on the y-axis.
* @return {goog.fx.ActiveDropTarget_} Dummy drop target.
* @private
*/
goog.fx.AbstractDragDrop.prototype.maybeCreateDummyTargetForPosition_ =
function(x, y) {
if (!this.dummyTarget_) {
this.dummyTarget_ = new goog.fx.ActiveDropTarget_(this.targetBox_.clone());
}
var fakeTargetBox = this.dummyTarget_.box_;
// Initialize the fake target box to the bounding box of DnD targets.
fakeTargetBox.top = this.targetBox_.top;
fakeTargetBox.right = this.targetBox_.right;
fakeTargetBox.bottom = this.targetBox_.bottom;
fakeTargetBox.left = this.targetBox_.left;
// Clip the fake target based on mouse position and DnD target boxes.
for (var i = 0, target; target = this.targetList_[i]; i++) {
var box = target.box_;
if (target.scrollableContainer_) {
// If the target has a scrollable container, use the intersection of that
// container's box and the target's box.
var scrollBox = target.scrollableContainer_.box_;
box = new goog.math.Box(
Math.max(box.top, scrollBox.top),
Math.min(box.right, scrollBox.right),
Math.min(box.bottom, scrollBox.bottom),
Math.max(box.left, scrollBox.left));
}
// Calculate clipping coordinates for horizontal and vertical axis.
// The clipping coordinate is calculated by projecting fake target box,
// the mouse pointer and DnD target box onto an axis and checking how
// box projections overlap and if the projected DnD target box contains
// mouse pointer. The clipping coordinate cannot be computed and is set to
// a negative value if the projected DnD target contains the mouse pointer.
var horizontalClip = null; // Assume mouse is above or below the DnD box.
if (x >= box.right) { // Mouse is to the right of the DnD box.
// Clip the fake box only if the DnD box overlaps it.
horizontalClip = box.right > fakeTargetBox.left ?
box.right : fakeTargetBox.left;
} else if (x < box.left) { // Mouse is to the left of the DnD box.
// Clip the fake box only if the DnD box overlaps it.
horizontalClip = box.left < fakeTargetBox.right ?
box.left : fakeTargetBox.right;
}
var verticalClip = null;
if (y >= box.bottom) {
verticalClip = box.bottom > fakeTargetBox.top ?
box.bottom : fakeTargetBox.top;
} else if (y < box.top) {
verticalClip = box.top < fakeTargetBox.bottom ?
box.top : fakeTargetBox.bottom;
}
// If both clippings are possible, choose one that gives us larger distance
// to mouse pointer (mark the shorter clipping as impossible, by setting it
// to null).
if (!goog.isNull(horizontalClip) && !goog.isNull(verticalClip)) {
if (Math.abs(horizontalClip - x) > Math.abs(verticalClip - y)) {
verticalClip = null;
} else {
horizontalClip = null;
}
}
// Clip none or one of fake target box sides (at most one clipping
// coordinate can be active).
if (!goog.isNull(horizontalClip)) {
if (horizontalClip <= x) {
fakeTargetBox.left = horizontalClip;
} else {
fakeTargetBox.right = horizontalClip;
}
} else if (!goog.isNull(verticalClip)) {
if (verticalClip <= y) {
fakeTargetBox.top = verticalClip;
} else {
fakeTargetBox.bottom = verticalClip;
}
}
}
// Only return the new fake target if it is big enough.
return (fakeTargetBox.right - fakeTargetBox.left) *
(fakeTargetBox.bottom - fakeTargetBox.top) >=
goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ ?
this.dummyTarget_ : null;
};
/**
* Returns the target for a given cursor position.
*
* @param {goog.math.Coordinate} position Cursor position.
* @return {Object} Target for position or null if no target was defined
* for the given position.
* @private
*/
goog.fx.AbstractDragDrop.prototype.getTargetFromPosition_ = function(position) {
for (var target, i = 0; target = this.targetList_[i]; i++) {
if (target.box_.contains(position)) {
if (target.scrollableContainer_) {
// If we have a scrollable container we will need to make sure
// we account for clipping of the scroll area
var box = target.scrollableContainer_.box_;
if (box.contains(position)) {
return target;
}
} else {
return target;
}
}
}
return null;
};
/**
* Checks whatever a given point is inside a given box.
*
* @param {number} x Cursor position on the x-axis.
* @param {number} y Cursor position on the y-axis.
* @param {goog.math.Box} box Box to check position against.
* @return {boolean} Whether the given point is inside {@code box}.
* @protected
* @deprecated Use goog.math.Box.contains.
*/
goog.fx.AbstractDragDrop.prototype.isInside = function(x, y, box) {
return x >= box.left &&
x < box.right &&
y >= box.top &&
y < box.bottom;
};
/**
* Gets the scroll distance as a coordinate object, using
* the window of the current drag element's dom.
* @return {!goog.math.Coordinate} Object with scroll offsets 'x' and 'y'.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.getScrollPos = function() {
return goog.dom.getDomHelper(this.dragEl_).getDocumentScroll();
};
/**
* Get the position of a drag event.
* @param {goog.fx.DragEvent} event Drag event.
* @return {!goog.math.Coordinate} Position of the event.
* @protected
*/
goog.fx.AbstractDragDrop.prototype.getEventPosition = function(event) {
var scroll = this.getScrollPos();
return new goog.math.Coordinate(event.clientX + scroll.x,
event.clientY + scroll.y);
};
/** @override */
goog.fx.AbstractDragDrop.prototype.disposeInternal = function() {
goog.fx.AbstractDragDrop.base(this, 'disposeInternal');
this.removeItems();
};
/**
* Object representing a drag and drop event.
*
* @param {string} type Event type.
* @param {goog.fx.AbstractDragDrop} source Source drag drop object.
* @param {goog.fx.DragDropItem} sourceItem Source item.
* @param {goog.fx.AbstractDragDrop=} opt_target Target drag drop object.
* @param {goog.fx.DragDropItem=} opt_targetItem Target item.
* @param {Element=} opt_targetElement Target element.
* @param {number=} opt_clientX X-Position relative to the screen.
* @param {number=} opt_clientY Y-Position relative to the screen.
* @param {number=} opt_x X-Position relative to the viewport.
* @param {number=} opt_y Y-Position relative to the viewport.
* @param {Object=} opt_subtarget The currently active subtarget.
* @extends {goog.events.Event}
* @constructor
*/
goog.fx.DragDropEvent = function(type, source, sourceItem,
opt_target, opt_targetItem, opt_targetElement,
opt_clientX, opt_clientY, opt_x, opt_y,
opt_subtarget) {
// TODO(eae): Get rid of all the optional parameters and have the caller set
// the fields directly instead.
goog.fx.DragDropEvent.base(this, 'constructor', type);
/**
* Reference to the source goog.fx.AbstractDragDrop object.
* @type {goog.fx.AbstractDragDrop}
*/
this.dragSource = source;
/**
* Reference to the source goog.fx.DragDropItem object.
* @type {goog.fx.DragDropItem}
*/
this.dragSourceItem = sourceItem;
/**
* Reference to the target goog.fx.AbstractDragDrop object.
* @type {goog.fx.AbstractDragDrop|undefined}
*/
this.dropTarget = opt_target;
/**
* Reference to the target goog.fx.DragDropItem object.
* @type {goog.fx.DragDropItem|undefined}
*/
this.dropTargetItem = opt_targetItem;
/**
* The actual element of the drop target that is the target for this event.
* @type {Element|undefined}
*/
this.dropTargetElement = opt_targetElement;
/**
* X-Position relative to the screen.
* @type {number|undefined}
*/
this.clientX = opt_clientX;
/**
* Y-Position relative to the screen.
* @type {number|undefined}
*/
this.clientY = opt_clientY;
/**
* X-Position relative to the viewport.
* @type {number|undefined}
*/
this.viewportX = opt_x;
/**
* Y-Position relative to the viewport.
* @type {number|undefined}
*/
this.viewportY = opt_y;
/**
* The subtarget that is currently active if a subtargeting function
* is supplied.
* @type {Object|undefined}
*/
this.subtarget = opt_subtarget;
};
goog.inherits(goog.fx.DragDropEvent, goog.events.Event);
/**
* Class representing a source or target element for drag and drop operations.
*
* @param {Element|string} element Dom Node, or string representation of node
* id, to be used as drag source/drop target.
* @param {Object=} opt_data Data associated with the source/target.
* @throws Error If no element argument is provided or if the type is invalid
* @extends {goog.events.EventTarget}
* @constructor
*/
goog.fx.DragDropItem = function(element, opt_data) {
goog.fx.DragDropItem.base(this, 'constructor');
/**
* Reference to drag source/target element
* @type {Element}
*/
this.element = goog.dom.getElement(element);
/**
* Data associated with element.
* @type {Object|undefined}
*/
this.data = opt_data;
/**
* Drag object the item belongs to.
* @type {goog.fx.AbstractDragDrop?}
* @private
*/
this.parent_ = null;
/**
* Event handler for listeners on events that can initiate a drag.
* @type {!goog.events.EventHandler<!goog.fx.DragDropItem>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
this.registerDisposable(this.eventHandler_);
if (!this.element) {
throw Error('Invalid argument');
}
};
goog.inherits(goog.fx.DragDropItem, goog.events.EventTarget);
/**
* The current element being dragged. This is needed because a DragDropItem can
* have multiple elements that can be dragged.
* @type {Element}
* @private
*/
goog.fx.DragDropItem.prototype.currentDragElement_ = null;
/**
* Get the data associated with the source/target.
* @return {Object|null|undefined} Data associated with the source/target.
*/
goog.fx.DragDropItem.prototype.getData = function() {
return this.data;
};
/**
* Gets the element that is actually draggable given that the given target was
* attempted to be dragged. This should be overriden when the element that was
* given actually contains many items that can be dragged. From the target, you
* can determine what element should actually be dragged.
*
* @param {Element} target The target that was attempted to be dragged.
* @return {Element} The element that is draggable given the target. If
* none are draggable, this will return null.
*/
goog.fx.DragDropItem.prototype.getDraggableElement = function(target) {
return target;
};
/**
* Gets the element that is currently being dragged.
*
* @return {Element} The element that is currently being dragged.
*/
goog.fx.DragDropItem.prototype.getCurrentDragElement = function() {
return this.currentDragElement_;
};
/**
* Gets all the elements of this item that are potentially draggable/
*
* @return {!Array<Element>} The draggable elements.
*/
goog.fx.DragDropItem.prototype.getDraggableElements = function() {
return [this.element];
};
/**
* Event handler for mouse down.
*
* @param {goog.events.BrowserEvent} event Mouse down event.
* @private
*/
goog.fx.DragDropItem.prototype.mouseDown_ = function(event) {
if (!event.isMouseActionButton()) {
return;
}
// Get the draggable element for the target.
var element = this.getDraggableElement(/** @type {Element} */ (event.target));
if (element) {
this.maybeStartDrag_(event, element);
}
};
/**
* Sets the dragdrop to which this item belongs.
* @param {goog.fx.AbstractDragDrop} parent The parent dragdrop.
*/
goog.fx.DragDropItem.prototype.setParent = function(parent) {
this.parent_ = parent;
};
/**
* Adds mouse move, mouse out and mouse up handlers.
*
* @param {goog.events.BrowserEvent} event Mouse down event.
* @param {Element} element Element.
* @private
*/
goog.fx.DragDropItem.prototype.maybeStartDrag_ = function(event, element) {
var eventType = goog.events.EventType;
this.eventHandler_.
listen(element, eventType.MOUSEMOVE, this.mouseMove_, false).
listen(element, eventType.MOUSEOUT, this.mouseMove_, false);
// Capture the MOUSEUP on the document to ensure that we cancel the start
// drag handlers even if the mouse up occurs on some other element. This can
// happen for instance when the mouse down changes the geometry of the element
// clicked on (e.g. through changes in activation styling) such that the mouse
// up occurs outside the original element.
var doc = goog.dom.getOwnerDocument(element);
this.eventHandler_.listen(doc, eventType.MOUSEUP, this.mouseUp_, true);
this.currentDragElement_ = element;
this.startPosition_ = new goog.math.Coordinate(
event.clientX, event.clientY);
event.preventDefault();
};
/**
* Event handler for mouse move. Starts drag operation if moved more than the
* threshold value.
*
* @param {goog.events.BrowserEvent} event Mouse move or mouse out event.
* @private
*/
goog.fx.DragDropItem.prototype.mouseMove_ = function(event) {
var distance = Math.abs(event.clientX - this.startPosition_.x) +
Math.abs(event.clientY - this.startPosition_.y);
// Fire dragStart event if the drag distance exceeds the threshold or if the
// mouse leave the dragged element.
// TODO(user): Consider using the goog.fx.Dragger to track the distance
// even after the mouse leaves the dragged element.
var currentDragElement = this.currentDragElement_;
var distanceAboveThreshold =
distance > goog.fx.AbstractDragDrop.initDragDistanceThreshold;
var mouseOutOnDragElement = event.type == goog.events.EventType.MOUSEOUT &&
event.target == currentDragElement;
if (distanceAboveThreshold || mouseOutOnDragElement) {
this.eventHandler_.removeAll();
this.parent_.startDrag(event, this);
}
};
/**
* Event handler for mouse up. Removes mouse move, mouse out and mouse up event
* handlers.
*
* @param {goog.events.BrowserEvent} event Mouse up event.
* @private
*/
goog.fx.DragDropItem.prototype.mouseUp_ = function(event) {
this.eventHandler_.removeAll();
delete this.startPosition_;
this.currentDragElement_ = null;
};
/**
* Class representing an active drop target
*
* @param {goog.math.Box} box Box describing the position and dimension of the
* target item.
* @param {goog.fx.AbstractDragDrop=} opt_target Target that contains the item
associated with position.
* @param {goog.fx.DragDropItem=} opt_item Item associated with position.
* @param {Element=} opt_element Element of item associated with position.
* @constructor
* @private
*/
goog.fx.ActiveDropTarget_ = function(box, opt_target, opt_item, opt_element) {
/**
* Box describing the position and dimension of the target item
* @type {goog.math.Box}
* @private
*/
this.box_ = box;
/**
* Target that contains the item associated with position
* @type {goog.fx.AbstractDragDrop|undefined}
* @private
*/
this.target_ = opt_target;
/**
* Item associated with position
* @type {goog.fx.DragDropItem|undefined}
* @private
*/
this.item_ = opt_item;
/**
* The draggable element of the item associated with position.
* @type {Element|undefined}
* @private
*/
this.element_ = opt_element;
};
/**
* If this target is in a scrollable container this is it.
* @type {goog.fx.ScrollableContainer_}
* @private
*/
goog.fx.ActiveDropTarget_.prototype.scrollableContainer_ = null;
/**
* Class for representing a scrollable container
* @param {Element} element the scrollable element.
* @constructor
* @private
*/
goog.fx.ScrollableContainer_ = function(element) {
/**
* The targets that lie within this container.
* @type {Array<goog.fx.ActiveDropTarget_>}
* @private
*/
this.containedTargets_ = [];
/**
* The element that is this container
* @type {Element}
* @private
*/
this.element_ = element;
/**
* The saved scroll left location for calculating deltas.
* @type {number}
* @private
*/
this.savedScrollLeft_ = 0;
/**
* The saved scroll top location for calculating deltas.
* @type {number}
* @private
*/
this.savedScrollTop_ = 0;
/**
* The space occupied by the container.
* @type {goog.math.Box}
* @private
*/
this.box_ = null;
};