/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Abstract Base Class for Drag and Drop.
*
* Provides functionality for implementing drag and drop classes. Also provides
* support classes and events.
*/
goog.provide('goog.fx.AbstractDragDrop');
goog.provide('goog.fx.AbstractDragDrop.EventType');
goog.provide('goog.fx.DragDropEvent');
goog.provide('goog.fx.DragDropItem');
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.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.requireType('goog.events.BrowserEvent');
goog.requireType('goog.fx.DragEvent');
/**
* 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 than a single 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 occurring.
*
* @extends {goog.events.EventTarget}
* @constructor
* @struct
*/
goog.fx.AbstractDragDrop = function() {
'use strict';
goog.fx.AbstractDragDrop.base(this, 'constructor');
/**
* List of items that makes up the drag source or drop target.
* @protected {Array<goog.fx.DragDropItem>}
* @suppress {underscore|visibility}
*/
this.items_ = [];
/**
* List of associated drop targets.
* @private {Array<goog.fx.AbstractDragDrop>}
*/
this.targets_ = [];
/**
* Scrollable containers to account for during drag
* @private {Array<goog.fx.ScrollableContainer_>}
*/
this.scrollableContainers_ = [];
/**
* Flag indicating if it's a drag source, set by addTarget.
* @private {boolean}
*/
this.isSource_ = false;
/**
* Flag indicating if it's a drop target, set when added as target to another
* DragDrop object.
* @private {boolean}
*/
this.isTarget_ = false;
/**
* Subtargeting function accepting args:
* (goog.fx.DragDropItem, goog.math.Box, number, number)
* @private {?Function}
*/
this.subtargetFunction_;
/**
* Last active subtarget.
* @private {?Object}
*/
this.activeSubtarget_;
/**
* Class name to add to source elements being dragged. Set by setDragClass.
* @private {?string}
*/
this.dragClass_;
/**
* Class name to add to source elements. Set by setSourceClass.
* @private {?string}
*/
this.sourceClass_;
/**
* Class name to add to target elements. Set by setTargetClass.
* @private {?string}
*/
this.targetClass_;
/**
* The SCROLL event target used to make drag element follow scrolling.
* @private {?EventTarget}
*/
this.scrollTarget_;
/**
* Dummy target, {@see maybeCreateDummyTargetForPosition_}.
* @private {?goog.fx.ActiveDropTarget_}
*/
this.dummyTarget_;
/**
* Whether the object has been initialized.
* @private {boolean}
*/
this.initialized_ = false;
/** @private {?Element} */
this.dragEl_;
/** @private {?Array<!goog.fx.ActiveDropTarget_>} */
this.targetList_;
/** @private {?goog.math.Box} */
this.targetBox_;
/** @private {?goog.fx.ActiveDropTarget_} */
this.activeTarget_;
/** @private {?goog.fx.DragDropItem} */
this.dragItem_;
/** @private {?goog.fx.Dragger} */
this.dragger_;
};
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;
/**
* 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) {
'use strict';
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) {
'use strict';
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) {
'use strict';
this.targetClass_ = className;
};
/**
* Whether the control has been initialized.
*
* @return {boolean} True if it's been initialized.
*/
goog.fx.AbstractDragDrop.prototype.isInitialized = function() {
'use strict';
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) {
'use strict';
this.targets_.push(target);
target.isTarget_ = true;
this.isSource_ = true;
};
/**
* Removes the specified target from the list of drop targets.
*
* @param {!goog.fx.AbstractDragDrop} target Target to remove.
*/
goog.fx.AbstractDragDrop.prototype.removeTarget = function(target) {
'use strict';
goog.array.remove(this.targets_, target);
if (this.activeTarget_ && this.activeTarget_.target_ == target) {
this.activeTarget_ = null;
}
this.recalculateDragTargets();
};
/**
* 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) {
'use strict';
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() {
'use strict';
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) {
'use strict';
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) {
'use strict';
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() {
'use strict';
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) {
'use strict';
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) {
'use strict';
// 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_,
undefined, // opt_target
undefined, // opt_targetItem
undefined, // opt_targetElement
undefined, // opt_clientX
undefined, // opt_clientY
undefined, // opt_x
undefined, // opt_y
undefined, // opt_subtarget
event);
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(/** @type {!Node} */ (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() {
'use strict';
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() {
'use strict';
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) {
'use strict';
// 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) {
'use strict';
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, event.browserEvent);
activeTarget.target_.dispatchEvent(dropEvent);
}
var dragEndEvent = new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAGEND, this, this.dragItem_,
activeTarget ? activeTarget.target_ : undefined,
activeTarget ? activeTarget.item_ : undefined,
activeTarget ? activeTarget.element_ : undefined);
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) {
'use strict';
this.disposeDrag();
};
/**
* Called once a drag operation has finished. Removes event listeners and
* elements.
*
* @protected
*/
goog.fx.AbstractDragDrop.prototype.disposeDrag = function() {
'use strict';
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) {
'use strict';
var position = this.getEventPosition(event);
var x = position.x;
var y = position.y;
var activeTarget = this.activeTarget_;
this.dispatchEvent(
new goog.fx.DragDropEvent(
goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,
activeTarget ? activeTarget.target_ : undefined,
activeTarget ? activeTarget.item_ : undefined,
activeTarget ? activeTarget.element_ : undefined, event.clientX,
event.clientY, x, 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 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) {
'use strict';
return false;
};
/**
* Sets up listeners for the scrollable containers that keep track of their
* scroll positions.
* @private
*/
goog.fx.AbstractDragDrop.prototype.initScrollableContainerListeners_ =
function() {
'use strict';
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() {
'use strict';
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) {
'use strict';
this.scrollableContainers_.push(new goog.fx.ScrollableContainer_(element));
};
/**
* Removes all scrollable containers.
*/
goog.fx.AbstractDragDrop.prototype.removeAllScrollableContainers = function() {
'use strict';
this.disposeScrollableContainerListeners_();
this.scrollableContainers_ = [];
};
/**
* Event handler for containers scrolling.
* @param {goog.events.BrowserEvent} e The event.
* @suppress {visibility} TODO(martone): update dependent projects.
* @private
*/
goog.fx.AbstractDragDrop.prototype.containerScrollHandler_ = function(e) {
'use strict';
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) {
'use strict';
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) {
'use strict';
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) {
'use strict';
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() {
'use strict';
return this.dragger_;
};
/**
* Creates copy of node being dragged.
*
* @param {Element} sourceEl Element to copy.
* @return {!Element} The clone of `sourceEl`.
* @deprecated Use goog.fx.Dragger.cloneNode().
* @private
*/
goog.fx.AbstractDragDrop.prototype.cloneNode_ = function(sourceEl) {
'use strict';
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
* `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) {
'use strict';
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) {
'use strict';
// Get all the draggable elements and add each one.
var draggableElements = item.getDraggableElements();
for (var i = 0; i < draggableElements.length; i++) {
var draggableElement = draggableElements[i];
// Determine target position and dimension
var box = this.getElementBox(item, draggableElement);
this.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) {
'use strict';
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) {
'use strict';
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) {
'use strict';
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 (horizontalClip !== null && verticalClip !== null) {
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 (horizontalClip !== null) {
if (horizontalClip <= x) {
fakeTargetBox.left = horizontalClip;
} else {
fakeTargetBox.right = horizontalClip;
}
} else if (verticalClip !== null) {
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 {goog.fx.ActiveDropTarget_} Target for position or null if no target
* was defined for the given position.
* @private
*/
goog.fx.AbstractDragDrop.prototype.getTargetFromPosition_ = function(position) {
'use strict';
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 `box`.
* @protected
* @deprecated Use goog.math.Box.contains.
*/
goog.fx.AbstractDragDrop.prototype.isInside = function(x, y, box) {
'use strict';
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() {
'use strict';
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) {
'use strict';
var scroll = this.getScrollPos();
return new goog.math.Coordinate(
event.clientX + scroll.x, event.clientY + scroll.y);
};
/**
* @override
* @protected
*/
goog.fx.AbstractDragDrop.prototype.disposeInternal = function() {
'use strict';
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.
* @param {goog.events.BrowserEvent=} opt_browserEvent The browser event
* that caused this dragdrop event.
* @extends {goog.events.Event}
* @constructor
* @struct
*/
goog.fx.DragDropEvent = function(
type, source, sourceItem, opt_target, opt_targetItem, opt_targetElement,
opt_clientX, opt_clientY, opt_x, opt_y, opt_subtarget, opt_browserEvent) {
'use strict';
// 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;
/**
* The browser event that caused this dragdrop event.
* @const
*/
this.browserEvent = opt_browserEvent;
};
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
* @struct
*/
goog.fx.DragDropItem = function(element, opt_data) {
'use strict';
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_);
/**
* The current element being dragged. This is needed because a DragDropItem
* can have multiple elements that can be dragged.
* @private {?Element}
*/
this.currentDragElement_ = null;
/** @private {?goog.math.Coordinate} */
this.startPosition_;
if (!this.element) {
throw new Error('Invalid argument');
}
};
goog.inherits(goog.fx.DragDropItem, goog.events.EventTarget);
/**
* Get the data associated with the source/target.
* @return {Object|null|undefined} Data associated with the source/target.
*/
goog.fx.DragDropItem.prototype.getData = function() {
'use strict';
return this.data;
};
/**
* Gets the element that is actually draggable given that the given target was
* attempted to be dragged. This should be overridden 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) {
'use strict';
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() {
'use strict';
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() {
'use strict';
return [this.element];
};
/**
* Event handler for mouse down.
*
* @param {goog.events.BrowserEvent} event Mouse down event.
* @private
*/
goog.fx.DragDropItem.prototype.mouseDown_ = function(event) {
'use strict';
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) {
'use strict';
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) {
'use strict';
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 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) {
'use strict';
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);
}
// Prevent text selection while dragging an element.
event.preventDefault();
};
/**
* 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) {
'use strict';
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
* @struct
* @private
*/
goog.fx.ActiveDropTarget_ = function(box, opt_target, opt_item, opt_element) {
'use strict';
/**
* 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}
* @private
*/
this.element_ = opt_element || null;
/**
* If this target is in a scrollable container this is it.
* @private {?goog.fx.ScrollableContainer_}
*/
this.scrollableContainer_ = null;
};
/**
* Class for representing a scrollable container
* @param {Element} element the scrollable element.
* @constructor
* @private
*/
goog.fx.ScrollableContainer_ = function(element) {
'use strict';
/**
* 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;
};
/**
* Test-only exports.
* @const
*/
goog.fx.AbstractDragDrop.TEST_ONLY = {
ActiveDropTarget: goog.fx.ActiveDropTarget_,
};