Path: blob/trunk/third_party/closure/goog/fx/abstractdragdrop.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Abstract Base Class for Drag and Drop.16*17* Provides functionality for implementing drag and drop classes. Also provides18* support classes and events.19*20* @author [email protected] (Emil A Eklund)21*/2223goog.provide('goog.fx.AbstractDragDrop');24goog.provide('goog.fx.AbstractDragDrop.EventType');25goog.provide('goog.fx.DragDropEvent');26goog.provide('goog.fx.DragDropItem');2728goog.require('goog.array');29goog.require('goog.asserts');30goog.require('goog.dom');31goog.require('goog.dom.classlist');32goog.require('goog.events');33goog.require('goog.events.Event');34goog.require('goog.events.EventHandler');35goog.require('goog.events.EventTarget');36goog.require('goog.events.EventType');37goog.require('goog.fx.Dragger');38goog.require('goog.math.Box');39goog.require('goog.math.Coordinate');40goog.require('goog.style');41424344/**45* Abstract class that provides reusable functionality for implementing drag46* and drop functionality.47*48* This class also allows clients to define their own subtargeting function49* so that drop areas can have finer granularity then a singe element. This is50* accomplished by using a client provided function to map from element and51* coordinates to a subregion id.52*53* This class can also be made aware of scrollable containers that contain54* drop targets by calling addScrollableContainer. This will cause dnd to55* take changing scroll positions into account while a drag is occurring.56*57* @extends {goog.events.EventTarget}58* @constructor59* @struct60*/61goog.fx.AbstractDragDrop = function() {62goog.fx.AbstractDragDrop.base(this, 'constructor');6364/**65* List of items that makes up the drag source or drop target.66* @protected {Array<goog.fx.DragDropItem>}67* @suppress {underscore|visibility}68*/69this.items_ = [];7071/**72* List of associated drop targets.73* @private {Array<goog.fx.AbstractDragDrop>}74*/75this.targets_ = [];7677/**78* Scrollable containers to account for during drag79* @private {Array<goog.fx.ScrollableContainer_>}80*/81this.scrollableContainers_ = [];8283/**84* Flag indicating if it's a drag source, set by addTarget.85* @private {boolean}86*/87this.isSource_ = false;8889/**90* Flag indicating if it's a drop target, set when added as target to another91* DragDrop object.92* @private {boolean}93*/94this.isTarget_ = false;9596/**97* Subtargeting function accepting args:98* (goog.fx.DragDropItem, goog.math.Box, number, number)99* @private {?Function}100*/101this.subtargetFunction_;102103/**104* Last active subtarget.105* @private {?Object}106*/107this.activeSubtarget_;108109/**110* Class name to add to source elements being dragged. Set by setDragClass.111* @private {?string}112*/113this.dragClass_;114115/**116* Class name to add to source elements. Set by setSourceClass.117* @private {?string}118*/119this.sourceClass_;120121/**122* Class name to add to target elements. Set by setTargetClass.123* @private {?string}124*/125this.targetClass_;126127/**128* The SCROLL event target used to make drag element follow scrolling.129* @private {?EventTarget}130*/131this.scrollTarget_;132133/**134* Dummy target, {@see maybeCreateDummyTargetForPosition_}.135* @private {?goog.fx.ActiveDropTarget_}136*/137this.dummyTarget_;138139/**140* Whether the object has been initialized.141* @private {boolean}142*/143this.initialized_ = false;144145/** @private {?Element} */146this.dragEl_;147148/** @private {?Array<!goog.fx.ActiveDropTarget_>} */149this.targetList_;150151/** @private {?goog.math.Box} */152this.targetBox_;153154/** @private {?goog.fx.ActiveDropTarget_} */155this.activeTarget_;156157/** @private {?goog.fx.DragDropItem} */158this.dragItem_;159160/** @private {?goog.fx.Dragger} */161this.dragger_;162};163goog.inherits(goog.fx.AbstractDragDrop, goog.events.EventTarget);164165166/**167* Minimum size (in pixels) for a dummy target. If the box for the target is168* less than the specified size it's not created.169* @type {number}170* @private171*/172goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ = 10;173174175/**176* Constants for event names177* @const178*/179goog.fx.AbstractDragDrop.EventType = {180DRAGOVER: 'dragover',181DRAGOUT: 'dragout',182DRAG: 'drag',183DROP: 'drop',184DRAGSTART: 'dragstart',185DRAGEND: 'dragend'186};187188189/**190* Constant for distance threshold, in pixels, an element has to be moved to191* initiate a drag operation.192* @type {number}193*/194goog.fx.AbstractDragDrop.initDragDistanceThreshold = 5;195196197/**198* Set class to add to source elements being dragged.199*200* @param {string} className Class to be added. Must be a single, valid201* classname.202*/203goog.fx.AbstractDragDrop.prototype.setDragClass = function(className) {204this.dragClass_ = className;205};206207208/**209* Set class to add to source elements.210*211* @param {string} className Class to be added. Must be a single, valid212* classname.213*/214goog.fx.AbstractDragDrop.prototype.setSourceClass = function(className) {215this.sourceClass_ = className;216};217218219/**220* Set class to add to target elements.221*222* @param {string} className Class to be added. Must be a single, valid223* classname.224*/225goog.fx.AbstractDragDrop.prototype.setTargetClass = function(className) {226this.targetClass_ = className;227};228229230/**231* Whether the control has been initialized.232*233* @return {boolean} True if it's been initialized.234*/235goog.fx.AbstractDragDrop.prototype.isInitialized = function() {236return this.initialized_;237};238239240/**241* Add item to drag object.242*243* @param {Element|string} element Dom Node, or string representation of node244* id, to be used as drag source/drop target.245* @throws Error Thrown if called on instance of abstract class246*/247goog.fx.AbstractDragDrop.prototype.addItem = goog.abstractMethod;248249250/**251* Associate drop target with drag element.252*253* @param {goog.fx.AbstractDragDrop} target Target to add.254*/255goog.fx.AbstractDragDrop.prototype.addTarget = function(target) {256this.targets_.push(target);257target.isTarget_ = true;258this.isSource_ = true;259};260261262/**263* Removes the specified target from the list of drop targets.264*265* @param {!goog.fx.AbstractDragDrop} target Target to remove.266*/267goog.fx.AbstractDragDrop.prototype.removeTarget = function(target) {268goog.array.remove(this.targets_, target);269if (this.activeTarget_ && this.activeTarget_.target_ == target) {270this.activeTarget_ = null;271}272this.recalculateDragTargets();273};274275276/**277* Sets the SCROLL event target to make drag element follow scrolling.278*279* @param {EventTarget} scrollTarget The element that dispatches SCROLL events.280*/281goog.fx.AbstractDragDrop.prototype.setScrollTarget = function(scrollTarget) {282this.scrollTarget_ = scrollTarget;283};284285286/**287* Initialize drag and drop functionality for sources/targets already added.288* Sources/targets added after init has been called will initialize themselves289* one by one.290*/291goog.fx.AbstractDragDrop.prototype.init = function() {292if (this.initialized_) {293return;294}295for (var item, i = 0; item = this.items_[i]; i++) {296this.initItem(item);297}298299this.initialized_ = true;300};301302303/**304* Initializes a single item.305*306* @param {goog.fx.DragDropItem} item Item to initialize.307* @protected308*/309goog.fx.AbstractDragDrop.prototype.initItem = function(item) {310if (this.isSource_) {311goog.events.listen(312item.element, goog.events.EventType.MOUSEDOWN, item.mouseDown_, false,313item);314if (this.sourceClass_) {315goog.dom.classlist.add(316goog.asserts.assert(item.element), this.sourceClass_);317}318}319320if (this.isTarget_ && this.targetClass_) {321goog.dom.classlist.add(322goog.asserts.assert(item.element), this.targetClass_);323}324};325326327/**328* Called when removing an item. Removes event listeners and classes.329*330* @param {goog.fx.DragDropItem} item Item to dispose.331* @protected332*/333goog.fx.AbstractDragDrop.prototype.disposeItem = function(item) {334if (this.isSource_) {335goog.events.unlisten(336item.element, goog.events.EventType.MOUSEDOWN, item.mouseDown_, false,337item);338if (this.sourceClass_) {339goog.dom.classlist.remove(340goog.asserts.assert(item.element), this.sourceClass_);341}342}343if (this.isTarget_ && this.targetClass_) {344goog.dom.classlist.remove(345goog.asserts.assert(item.element), this.targetClass_);346}347item.dispose();348};349350351/**352* Removes all items.353*/354goog.fx.AbstractDragDrop.prototype.removeItems = function() {355for (var item, i = 0; item = this.items_[i]; i++) {356this.disposeItem(item);357}358this.items_.length = 0;359};360361362/**363* Starts a drag event for an item if the mouse button stays pressed and the364* cursor moves a few pixels. Allows dragging of items without first having to365* register them with addItem.366*367* @param {goog.events.BrowserEvent} event Mouse down event.368* @param {goog.fx.DragDropItem} item Item that's being dragged.369*/370goog.fx.AbstractDragDrop.prototype.maybeStartDrag = function(event, item) {371item.maybeStartDrag_(event, item.element);372};373374375/**376* Event handler that's used to start drag.377*378* @param {goog.events.BrowserEvent} event Mouse move event.379* @param {goog.fx.DragDropItem} item Item that's being dragged.380*/381goog.fx.AbstractDragDrop.prototype.startDrag = function(event, item) {382383// Prevent a new drag operation from being started if another one is already384// in progress (could happen if the mouse was released outside of the385// document).386if (this.dragItem_) {387return;388}389390this.dragItem_ = item;391392// Dispatch DRAGSTART event393var dragStartEvent = new goog.fx.DragDropEvent(394goog.fx.AbstractDragDrop.EventType.DRAGSTART, this, this.dragItem_,395undefined, // opt_target396undefined, // opt_targetItem397undefined, // opt_targetElement398undefined, // opt_clientX399undefined, // opt_clientY400undefined, // opt_x401undefined, // opt_y402undefined, // opt_subtarget403event);404if (this.dispatchEvent(dragStartEvent) == false) {405this.dragItem_ = null;406return;407}408409// Get the source element and create a drag element for it.410var el = item.getCurrentDragElement();411this.dragEl_ = this.createDragElement(el);412var doc = goog.dom.getOwnerDocument(el);413doc.body.appendChild(this.dragEl_);414415this.dragger_ = this.createDraggerFor(el, this.dragEl_, event);416this.dragger_.setScrollTarget(this.scrollTarget_);417418goog.events.listen(419this.dragger_, goog.fx.Dragger.EventType.DRAG, this.moveDrag_, false,420this);421422goog.events.listen(423this.dragger_, goog.fx.Dragger.EventType.END, this.endDrag, false, this);424425// IE may issue a 'selectstart' event when dragging over an iframe even when426// default mousemove behavior is suppressed. If the default selectstart427// behavior is not suppressed, elements dragged over will show as selected.428goog.events.listen(429doc.body, goog.events.EventType.SELECTSTART, this.suppressSelect_);430431this.recalculateDragTargets();432this.recalculateScrollableContainers();433this.activeTarget_ = null;434this.initScrollableContainerListeners_();435this.dragger_.startDrag(event);436437event.preventDefault();438};439440441/**442* Recalculates the geometry of this source's drag targets. Call this443* if the position or visibility of a drag target has changed during444* a drag, or if targets are added or removed.445*446* TODO(user): this is an expensive operation; more efficient APIs447* may be necessary.448*/449goog.fx.AbstractDragDrop.prototype.recalculateDragTargets = function() {450this.targetList_ = [];451for (var target, i = 0; target = this.targets_[i]; i++) {452for (var itm, j = 0; itm = target.items_[j]; j++) {453this.addDragTarget_(target, itm);454}455}456if (!this.targetBox_) {457this.targetBox_ = new goog.math.Box(0, 0, 0, 0);458}459};460461462/**463* Recalculates the current scroll positions of scrollable containers and464* allocates targets. Call this if the position of a container changed or if465* targets are added or removed.466*/467goog.fx.AbstractDragDrop.prototype.recalculateScrollableContainers =468function() {469var container, i, j, target;470for (i = 0; container = this.scrollableContainers_[i]; i++) {471container.containedTargets_ = [];472container.savedScrollLeft_ = container.element_.scrollLeft;473container.savedScrollTop_ = container.element_.scrollTop;474var pos = goog.style.getPageOffset(container.element_);475var size = goog.style.getSize(container.element_);476container.box_ = new goog.math.Box(477pos.y, pos.x + size.width, pos.y + size.height, pos.x);478}479480for (i = 0; target = this.targetList_[i]; i++) {481for (j = 0; container = this.scrollableContainers_[j]; j++) {482if (goog.dom.contains(container.element_, target.element_)) {483container.containedTargets_.push(target);484target.scrollableContainer_ = container;485}486}487}488};489490491/**492* Creates the Dragger for the drag element.493* @param {Element} sourceEl Drag source element.494* @param {Element} el the element created by createDragElement().495* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.496* @return {!goog.fx.Dragger} The new Dragger.497* @protected498*/499goog.fx.AbstractDragDrop.prototype.createDraggerFor = function(500sourceEl, el, event) {501// Position the drag element.502var pos = this.getDragElementPosition(sourceEl, el, event);503el.style.position = 'absolute';504el.style.left = pos.x + 'px';505el.style.top = pos.y + 'px';506return new goog.fx.Dragger(el);507};508509510/**511* Event handler that's used to stop drag. Fires a drop event if over a valid512* target.513*514* @param {goog.fx.DragEvent} event Drag event.515*/516goog.fx.AbstractDragDrop.prototype.endDrag = function(event) {517var activeTarget = event.dragCanceled ? null : this.activeTarget_;518if (activeTarget && activeTarget.target_) {519var clientX = event.clientX;520var clientY = event.clientY;521var scroll = this.getScrollPos();522var x = clientX + scroll.x;523var y = clientY + scroll.y;524525var subtarget;526// If a subtargeting function is enabled get the current subtarget527if (this.subtargetFunction_) {528subtarget =529this.subtargetFunction_(activeTarget.item_, activeTarget.box_, x, y);530}531532var dragEvent = new goog.fx.DragDropEvent(533goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,534activeTarget.target_, activeTarget.item_, activeTarget.element_,535clientX, clientY, x, y);536this.dispatchEvent(dragEvent);537538var dropEvent = new goog.fx.DragDropEvent(539goog.fx.AbstractDragDrop.EventType.DROP, this, this.dragItem_,540activeTarget.target_, activeTarget.item_, activeTarget.element_,541clientX, clientY, x, y, subtarget, event.browserEvent);542activeTarget.target_.dispatchEvent(dropEvent);543}544545var dragEndEvent = new goog.fx.DragDropEvent(546goog.fx.AbstractDragDrop.EventType.DRAGEND, this, this.dragItem_,547activeTarget ? activeTarget.target_ : undefined,548activeTarget ? activeTarget.item_ : undefined,549activeTarget ? activeTarget.element_ : undefined);550this.dispatchEvent(dragEndEvent);551552goog.events.unlisten(553this.dragger_, goog.fx.Dragger.EventType.DRAG, this.moveDrag_, false,554this);555goog.events.unlisten(556this.dragger_, goog.fx.Dragger.EventType.END, this.endDrag, false, this);557var doc = goog.dom.getOwnerDocument(this.dragItem_.getCurrentDragElement());558goog.events.unlisten(559doc.body, goog.events.EventType.SELECTSTART, this.suppressSelect_);560561562this.afterEndDrag(this.activeTarget_ ? this.activeTarget_.item_ : null);563};564565566/**567* Called after a drag operation has finished.568*569* @param {goog.fx.DragDropItem=} opt_dropTarget Target for successful drop.570* @protected571*/572goog.fx.AbstractDragDrop.prototype.afterEndDrag = function(opt_dropTarget) {573this.disposeDrag();574};575576577/**578* Called once a drag operation has finished. Removes event listeners and579* elements.580*581* @protected582*/583goog.fx.AbstractDragDrop.prototype.disposeDrag = function() {584this.disposeScrollableContainerListeners_();585this.dragger_.dispose();586587goog.dom.removeNode(this.dragEl_);588delete this.dragItem_;589delete this.dragEl_;590delete this.dragger_;591delete this.targetList_;592delete this.activeTarget_;593};594595596/**597* Event handler for drag events. Determines the active drop target, if any, and598* fires dragover and dragout events appropriately.599*600* @param {goog.fx.DragEvent} event Drag event.601* @private602*/603goog.fx.AbstractDragDrop.prototype.moveDrag_ = function(event) {604var position = this.getEventPosition(event);605var x = position.x;606var y = position.y;607608var activeTarget = this.activeTarget_;609610this.dispatchEvent(611new goog.fx.DragDropEvent(612goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,613activeTarget ? activeTarget.target_ : undefined,614activeTarget ? activeTarget.item_ : undefined,615activeTarget ? activeTarget.element_ : undefined, event.clientX,616event.clientY, x, y));617618// Check if we're still inside the bounds of the active target, if not fire619// a dragout event and proceed to find a new target.620var subtarget;621if (activeTarget) {622// If a subtargeting function is enabled get the current subtarget623if (this.subtargetFunction_ && activeTarget.target_) {624subtarget =625this.subtargetFunction_(activeTarget.item_, activeTarget.box_, x, y);626}627628if (activeTarget.box_.contains(position) &&629subtarget == this.activeSubtarget_) {630return;631}632633if (activeTarget.target_) {634var sourceDragOutEvent = new goog.fx.DragDropEvent(635goog.fx.AbstractDragDrop.EventType.DRAGOUT, this, this.dragItem_,636activeTarget.target_, activeTarget.item_, activeTarget.element_);637this.dispatchEvent(sourceDragOutEvent);638639// The event should be dispatched the by target DragDrop so that the640// target DragDrop can manage these events without having to know what641// sources this is a target for.642var targetDragOutEvent = new goog.fx.DragDropEvent(643goog.fx.AbstractDragDrop.EventType.DRAGOUT, this, this.dragItem_,644activeTarget.target_, activeTarget.item_, activeTarget.element_,645undefined, undefined, undefined, undefined, this.activeSubtarget_);646activeTarget.target_.dispatchEvent(targetDragOutEvent);647}648this.activeSubtarget_ = subtarget;649this.activeTarget_ = null;650}651652// Check if inside target box653if (this.targetBox_.contains(position)) {654// Search for target and fire a dragover event if found655activeTarget = this.activeTarget_ = this.getTargetFromPosition_(position);656if (activeTarget && activeTarget.target_) {657// If a subtargeting function is enabled get the current subtarget658if (this.subtargetFunction_) {659subtarget = this.subtargetFunction_(660activeTarget.item_, activeTarget.box_, x, y);661}662var sourceDragOverEvent = new goog.fx.DragDropEvent(663goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,664activeTarget.target_, activeTarget.item_, activeTarget.element_);665sourceDragOverEvent.subtarget = subtarget;666this.dispatchEvent(sourceDragOverEvent);667668// The event should be dispatched by the target DragDrop so that the669// target DragDrop can manage these events without having to know what670// sources this is a target for.671var targetDragOverEvent = new goog.fx.DragDropEvent(672goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,673activeTarget.target_, activeTarget.item_, activeTarget.element_,674event.clientX, event.clientY, undefined, undefined, subtarget);675activeTarget.target_.dispatchEvent(targetDragOverEvent);676677} else if (!activeTarget) {678// If no target was found create a dummy one so we won't have to iterate679// over all possible targets for every move event.680this.activeTarget_ = this.maybeCreateDummyTargetForPosition_(x, y);681}682}683};684685686/**687* Event handler for suppressing selectstart events. Selecting should be688* disabled while dragging.689*690* @param {goog.events.Event} event The selectstart event to suppress.691* @return {boolean} Whether to perform default behavior.692* @private693*/694goog.fx.AbstractDragDrop.prototype.suppressSelect_ = function(event) {695return false;696};697698699/**700* Sets up listeners for the scrollable containers that keep track of their701* scroll positions.702* @private703*/704goog.fx.AbstractDragDrop.prototype.initScrollableContainerListeners_ =705function() {706var container, i;707for (i = 0; container = this.scrollableContainers_[i]; i++) {708goog.events.listen(709container.element_, goog.events.EventType.SCROLL,710this.containerScrollHandler_, false, this);711}712};713714715/**716* Cleans up the scrollable container listeners.717* @private718*/719goog.fx.AbstractDragDrop.prototype.disposeScrollableContainerListeners_ =720function() {721for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {722goog.events.unlisten(723container.element_, 'scroll', this.containerScrollHandler_, false,724this);725container.containedTargets_ = [];726}727};728729730/**731* Makes drag and drop aware of a target container that could scroll mid drag.732* @param {Element} element The scroll container.733*/734goog.fx.AbstractDragDrop.prototype.addScrollableContainer = function(element) {735this.scrollableContainers_.push(new goog.fx.ScrollableContainer_(element));736};737738739/**740* Removes all scrollable containers.741*/742goog.fx.AbstractDragDrop.prototype.removeAllScrollableContainers = function() {743this.disposeScrollableContainerListeners_();744this.scrollableContainers_ = [];745};746747748/**749* Event handler for containers scrolling.750* @param {goog.events.BrowserEvent} e The event.751* @suppress {visibility} TODO(martone): update dependent projects.752* @private753*/754goog.fx.AbstractDragDrop.prototype.containerScrollHandler_ = function(e) {755for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {756if (e.target == container.element_) {757var deltaTop = container.savedScrollTop_ - container.element_.scrollTop;758var deltaLeft =759container.savedScrollLeft_ - container.element_.scrollLeft;760container.savedScrollTop_ = container.element_.scrollTop;761container.savedScrollLeft_ = container.element_.scrollLeft;762763// When the container scrolls, it's possible that one of the targets will764// move to the region contained by the dummy target. Since we don't know765// which sides (if any) of the dummy target are defined by targets766// contained by this container, we are conservative and just shrink it.767if (this.dummyTarget_ && this.activeTarget_ == this.dummyTarget_) {768if (deltaTop > 0) {769this.dummyTarget_.box_.top += deltaTop;770} else {771this.dummyTarget_.box_.bottom += deltaTop;772}773if (deltaLeft > 0) {774this.dummyTarget_.box_.left += deltaLeft;775} else {776this.dummyTarget_.box_.right += deltaLeft;777}778}779for (var j = 0, target; target = container.containedTargets_[j]; j++) {780var box = target.box_;781box.top += deltaTop;782box.left += deltaLeft;783box.bottom += deltaTop;784box.right += deltaLeft;785786this.calculateTargetBox_(box);787}788}789}790this.dragger_.onScroll_(e);791};792793794/**795* Set a function that provides subtargets. A subtargeting function796* returns an arbitrary identifier for each subtarget of an element.797* DnD code will generate additional drag over / out events when798* switching from subtarget to subtarget. This is useful for instance799* if you are interested if you are on the top half or the bottom half800* of the element.801* The provided function will be given the DragDropItem, box, x, y802* box is the current window coordinates occupied by element803* x, y is the mouse position in window coordinates804*805* @param {Function} f The new subtarget function.806*/807goog.fx.AbstractDragDrop.prototype.setSubtargetFunction = function(f) {808this.subtargetFunction_ = f;809};810811812/**813* Creates an element for the item being dragged.814*815* @param {Element} sourceEl Drag source element.816* @return {Element} The new drag element.817*/818goog.fx.AbstractDragDrop.prototype.createDragElement = function(sourceEl) {819var dragEl = this.createDragElementInternal(sourceEl);820goog.asserts.assert(dragEl);821if (this.dragClass_) {822goog.dom.classlist.add(dragEl, this.dragClass_);823}824825return dragEl;826};827828829/**830* Returns the position for the drag element.831*832* @param {Element} el Drag source element.833* @param {Element} dragEl The dragged element created by createDragElement().834* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.835* @return {!goog.math.Coordinate} The position for the drag element.836*/837goog.fx.AbstractDragDrop.prototype.getDragElementPosition = function(838el, dragEl, event) {839var pos = goog.style.getPageOffset(el);840841// Subtract margin from drag element position twice, once to adjust the842// position given by the original node and once for the drag node.843var marginBox = goog.style.getMarginBox(el);844pos.x -= (marginBox.left || 0) * 2;845pos.y -= (marginBox.top || 0) * 2;846847return pos;848};849850851/**852* Returns the dragger object.853*854* @return {goog.fx.Dragger} The dragger object used by this drag and drop855* instance.856*/857goog.fx.AbstractDragDrop.prototype.getDragger = function() {858return this.dragger_;859};860861862/**863* Creates copy of node being dragged.864*865* @param {Element} sourceEl Element to copy.866* @return {!Element} The clone of {@code sourceEl}.867* @deprecated Use goog.fx.Dragger.cloneNode().868* @private869*/870goog.fx.AbstractDragDrop.prototype.cloneNode_ = function(sourceEl) {871return goog.fx.Dragger.cloneNode(sourceEl);872};873874875/**876* Generates an element to follow the cursor during dragging, given a drag877* source element. The default behavior is simply to clone the source element,878* but this may be overridden in subclasses. This method is called by879* {@code createDragElement()} before the drag class is added.880*881* @param {Element} sourceEl Drag source element.882* @return {!Element} The new drag element.883* @protected884* @suppress {deprecated}885*/886goog.fx.AbstractDragDrop.prototype.createDragElementInternal = function(887sourceEl) {888return this.cloneNode_(sourceEl);889};890891892/**893* Add possible drop target for current drag operation.894*895* @param {goog.fx.AbstractDragDrop} target Drag handler.896* @param {goog.fx.DragDropItem} item Item that's being dragged.897* @private898*/899goog.fx.AbstractDragDrop.prototype.addDragTarget_ = function(target, item) {900901// Get all the draggable elements and add each one.902var draggableElements = item.getDraggableElements();903for (var i = 0; i < draggableElements.length; i++) {904var draggableElement = draggableElements[i];905906// Determine target position and dimension907var box = this.getElementBox(item, draggableElement);908909this.targetList_.push(910new goog.fx.ActiveDropTarget_(box, target, item, draggableElement));911912this.calculateTargetBox_(box);913}914};915916917/**918* Calculates the position and dimension of a draggable element.919*920* @param {goog.fx.DragDropItem} item Item that's being dragged.921* @param {Element} element The element to calculate the box.922*923* @return {!goog.math.Box} Box describing the position and dimension924* of element.925* @protected926*/927goog.fx.AbstractDragDrop.prototype.getElementBox = function(item, element) {928var pos = goog.style.getPageOffset(element);929var size = goog.style.getSize(element);930return new goog.math.Box(931pos.y, pos.x + size.width, pos.y + size.height, pos.x);932};933934935/**936* Calculate the outer bounds (the region all targets are inside).937*938* @param {goog.math.Box} box Box describing the position and dimension939* of a drag target.940* @private941*/942goog.fx.AbstractDragDrop.prototype.calculateTargetBox_ = function(box) {943if (this.targetList_.length == 1) {944this.targetBox_ =945new goog.math.Box(box.top, box.right, box.bottom, box.left);946} else {947var tb = this.targetBox_;948tb.left = Math.min(box.left, tb.left);949tb.right = Math.max(box.right, tb.right);950tb.top = Math.min(box.top, tb.top);951tb.bottom = Math.max(box.bottom, tb.bottom);952}953};954955956/**957* Creates a dummy target for the given cursor position. The assumption is to958* create as big dummy target box as possible, the only constraints are:959* - The dummy target box cannot overlap any of real target boxes.960* - The dummy target has to contain a point with current mouse coordinates.961*962* NOTE: For performance reasons the box construction algorithm is kept simple963* and it is not optimal (see example below). Currently it is O(n) in regard to964* the number of real drop target boxes, but its result depends on the order965* of those boxes being processed (the order in which they're added to the966* targetList_ collection).967*968* The algorithm.969* a) Assumptions970* - Mouse pointer is in the bounding box of real target boxes.971* - None of the boxes have negative coordinate values.972* - Mouse pointer is not contained by any of "real target" boxes.973* - For targets inside a scrollable container, the box used is the974* intersection of the scrollable container's box and the target's box.975* This is because the part of the target that extends outside the scrollable976* container should not be used in the clipping calculations.977*978* b) Outline979* - Initialize the fake target to the bounding box of real targets.980* - For each real target box - clip the fake target box so it does not contain981* that target box, but does contain the mouse pointer.982* -- Project the real target box, mouse pointer and fake target box onto983* both axes and calculate the clipping coordinates.984* -- Only one coordinate is used to clip the fake target box to keep the985* fake target as big as possible.986* -- If the projection of the real target box contains the mouse pointer,987* clipping for a given axis is not possible.988* -- If both clippings are possible, the clipping more distant from the989* mouse pointer is selected to keep bigger fake target area.990* - Save the created fake target only if it has a big enough area.991*992*993* c) Example994* <pre>995* Input: Algorithm created box: Maximum box:996* +---------------------+ +---------------------+ +---------------------+997* | B1 | B2 | | B1 B2 | | B1 B2 |998* | | | | +-------------+ | |+-------------------+|999* |---------x-----------| | | | | || ||1000* | | | | | | | || ||1001* | | | | | | | || ||1002* | | | | | | | || ||1003* | | | | | | | || ||1004* | | | | +-------------+ | |+-------------------+|1005* | B4 | B3 | | B4 B3 | | B4 B3 |1006* +---------------------+ +---------------------+ +---------------------+1007* </pre>1008*1009* @param {number} x Cursor position on the x-axis.1010* @param {number} y Cursor position on the y-axis.1011* @return {goog.fx.ActiveDropTarget_} Dummy drop target.1012* @private1013*/1014goog.fx.AbstractDragDrop.prototype.maybeCreateDummyTargetForPosition_ =1015function(x, y) {1016if (!this.dummyTarget_) {1017this.dummyTarget_ = new goog.fx.ActiveDropTarget_(this.targetBox_.clone());1018}1019var fakeTargetBox = this.dummyTarget_.box_;10201021// Initialize the fake target box to the bounding box of DnD targets.1022fakeTargetBox.top = this.targetBox_.top;1023fakeTargetBox.right = this.targetBox_.right;1024fakeTargetBox.bottom = this.targetBox_.bottom;1025fakeTargetBox.left = this.targetBox_.left;10261027// Clip the fake target based on mouse position and DnD target boxes.1028for (var i = 0, target; target = this.targetList_[i]; i++) {1029var box = target.box_;10301031if (target.scrollableContainer_) {1032// If the target has a scrollable container, use the intersection of that1033// container's box and the target's box.1034var scrollBox = target.scrollableContainer_.box_;10351036box = new goog.math.Box(1037Math.max(box.top, scrollBox.top),1038Math.min(box.right, scrollBox.right),1039Math.min(box.bottom, scrollBox.bottom),1040Math.max(box.left, scrollBox.left));1041}10421043// Calculate clipping coordinates for horizontal and vertical axis.1044// The clipping coordinate is calculated by projecting fake target box,1045// the mouse pointer and DnD target box onto an axis and checking how1046// box projections overlap and if the projected DnD target box contains1047// mouse pointer. The clipping coordinate cannot be computed and is set to1048// a negative value if the projected DnD target contains the mouse pointer.10491050var horizontalClip = null; // Assume mouse is above or below the DnD box.1051if (x >= box.right) { // Mouse is to the right of the DnD box.1052// Clip the fake box only if the DnD box overlaps it.1053horizontalClip =1054box.right > fakeTargetBox.left ? box.right : fakeTargetBox.left;1055} else if (x < box.left) { // Mouse is to the left of the DnD box.1056// Clip the fake box only if the DnD box overlaps it.1057horizontalClip =1058box.left < fakeTargetBox.right ? box.left : fakeTargetBox.right;1059}1060var verticalClip = null;1061if (y >= box.bottom) {1062verticalClip =1063box.bottom > fakeTargetBox.top ? box.bottom : fakeTargetBox.top;1064} else if (y < box.top) {1065verticalClip =1066box.top < fakeTargetBox.bottom ? box.top : fakeTargetBox.bottom;1067}10681069// If both clippings are possible, choose one that gives us larger distance1070// to mouse pointer (mark the shorter clipping as impossible, by setting it1071// to null).1072if (!goog.isNull(horizontalClip) && !goog.isNull(verticalClip)) {1073if (Math.abs(horizontalClip - x) > Math.abs(verticalClip - y)) {1074verticalClip = null;1075} else {1076horizontalClip = null;1077}1078}10791080// Clip none or one of fake target box sides (at most one clipping1081// coordinate can be active).1082if (!goog.isNull(horizontalClip)) {1083if (horizontalClip <= x) {1084fakeTargetBox.left = horizontalClip;1085} else {1086fakeTargetBox.right = horizontalClip;1087}1088} else if (!goog.isNull(verticalClip)) {1089if (verticalClip <= y) {1090fakeTargetBox.top = verticalClip;1091} else {1092fakeTargetBox.bottom = verticalClip;1093}1094}1095}10961097// Only return the new fake target if it is big enough.1098return (fakeTargetBox.right - fakeTargetBox.left) *1099(fakeTargetBox.bottom - fakeTargetBox.top) >=1100goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ ?1101this.dummyTarget_ :1102null;1103};110411051106/**1107* Returns the target for a given cursor position.1108*1109* @param {goog.math.Coordinate} position Cursor position.1110* @return {goog.fx.ActiveDropTarget_} Target for position or null if no target1111* was defined for the given position.1112* @private1113*/1114goog.fx.AbstractDragDrop.prototype.getTargetFromPosition_ = function(position) {1115for (var target, i = 0; target = this.targetList_[i]; i++) {1116if (target.box_.contains(position)) {1117if (target.scrollableContainer_) {1118// If we have a scrollable container we will need to make sure1119// we account for clipping of the scroll area1120var box = target.scrollableContainer_.box_;1121if (box.contains(position)) {1122return target;1123}1124} else {1125return target;1126}1127}1128}11291130return null;1131};113211331134/**1135* Checks whatever a given point is inside a given box.1136*1137* @param {number} x Cursor position on the x-axis.1138* @param {number} y Cursor position on the y-axis.1139* @param {goog.math.Box} box Box to check position against.1140* @return {boolean} Whether the given point is inside {@code box}.1141* @protected1142* @deprecated Use goog.math.Box.contains.1143*/1144goog.fx.AbstractDragDrop.prototype.isInside = function(x, y, box) {1145return x >= box.left && x < box.right && y >= box.top && y < box.bottom;1146};114711481149/**1150* Gets the scroll distance as a coordinate object, using1151* the window of the current drag element's dom.1152* @return {!goog.math.Coordinate} Object with scroll offsets 'x' and 'y'.1153* @protected1154*/1155goog.fx.AbstractDragDrop.prototype.getScrollPos = function() {1156return goog.dom.getDomHelper(this.dragEl_).getDocumentScroll();1157};115811591160/**1161* Get the position of a drag event.1162* @param {goog.fx.DragEvent} event Drag event.1163* @return {!goog.math.Coordinate} Position of the event.1164* @protected1165*/1166goog.fx.AbstractDragDrop.prototype.getEventPosition = function(event) {1167var scroll = this.getScrollPos();1168return new goog.math.Coordinate(1169event.clientX + scroll.x, event.clientY + scroll.y);1170};117111721173/** @override */1174goog.fx.AbstractDragDrop.prototype.disposeInternal = function() {1175goog.fx.AbstractDragDrop.base(this, 'disposeInternal');1176this.removeItems();1177};1178117911801181/**1182* Object representing a drag and drop event.1183*1184* @param {string} type Event type.1185* @param {goog.fx.AbstractDragDrop} source Source drag drop object.1186* @param {goog.fx.DragDropItem} sourceItem Source item.1187* @param {goog.fx.AbstractDragDrop=} opt_target Target drag drop object.1188* @param {goog.fx.DragDropItem=} opt_targetItem Target item.1189* @param {Element=} opt_targetElement Target element.1190* @param {number=} opt_clientX X-Position relative to the screen.1191* @param {number=} opt_clientY Y-Position relative to the screen.1192* @param {number=} opt_x X-Position relative to the viewport.1193* @param {number=} opt_y Y-Position relative to the viewport.1194* @param {Object=} opt_subtarget The currently active subtarget.1195* @param {goog.events.BrowserEvent=} opt_browserEvent The browser event1196* that caused this dragdrop event.1197* @extends {goog.events.Event}1198* @constructor1199* @struct1200*/1201goog.fx.DragDropEvent = function(1202type, source, sourceItem, opt_target, opt_targetItem, opt_targetElement,1203opt_clientX, opt_clientY, opt_x, opt_y, opt_subtarget, opt_browserEvent) {1204// TODO(eae): Get rid of all the optional parameters and have the caller set1205// the fields directly instead.1206goog.fx.DragDropEvent.base(this, 'constructor', type);12071208/**1209* Reference to the source goog.fx.AbstractDragDrop object.1210* @type {goog.fx.AbstractDragDrop}1211*/1212this.dragSource = source;12131214/**1215* Reference to the source goog.fx.DragDropItem object.1216* @type {goog.fx.DragDropItem}1217*/1218this.dragSourceItem = sourceItem;12191220/**1221* Reference to the target goog.fx.AbstractDragDrop object.1222* @type {goog.fx.AbstractDragDrop|undefined}1223*/1224this.dropTarget = opt_target;12251226/**1227* Reference to the target goog.fx.DragDropItem object.1228* @type {goog.fx.DragDropItem|undefined}1229*/1230this.dropTargetItem = opt_targetItem;12311232/**1233* The actual element of the drop target that is the target for this event.1234* @type {Element|undefined}1235*/1236this.dropTargetElement = opt_targetElement;12371238/**1239* X-Position relative to the screen.1240* @type {number|undefined}1241*/1242this.clientX = opt_clientX;12431244/**1245* Y-Position relative to the screen.1246* @type {number|undefined}1247*/1248this.clientY = opt_clientY;12491250/**1251* X-Position relative to the viewport.1252* @type {number|undefined}1253*/1254this.viewportX = opt_x;12551256/**1257* Y-Position relative to the viewport.1258* @type {number|undefined}1259*/1260this.viewportY = opt_y;12611262/**1263* The subtarget that is currently active if a subtargeting function1264* is supplied.1265* @type {Object|undefined}1266*/1267this.subtarget = opt_subtarget;12681269/**1270* The browser event that caused this dragdrop event.1271* @const1272*/1273this.browserEvent = opt_browserEvent;1274};1275goog.inherits(goog.fx.DragDropEvent, goog.events.Event);1276127712781279/**1280* Class representing a source or target element for drag and drop operations.1281*1282* @param {Element|string} element Dom Node, or string representation of node1283* id, to be used as drag source/drop target.1284* @param {Object=} opt_data Data associated with the source/target.1285* @throws Error If no element argument is provided or if the type is invalid1286* @extends {goog.events.EventTarget}1287* @constructor1288* @struct1289*/1290goog.fx.DragDropItem = function(element, opt_data) {1291goog.fx.DragDropItem.base(this, 'constructor');12921293/**1294* Reference to drag source/target element1295* @type {Element}1296*/1297this.element = goog.dom.getElement(element);12981299/**1300* Data associated with element.1301* @type {Object|undefined}1302*/1303this.data = opt_data;13041305/**1306* Drag object the item belongs to.1307* @type {goog.fx.AbstractDragDrop?}1308* @private1309*/1310this.parent_ = null;13111312/**1313* Event handler for listeners on events that can initiate a drag.1314* @type {!goog.events.EventHandler<!goog.fx.DragDropItem>}1315* @private1316*/1317this.eventHandler_ = new goog.events.EventHandler(this);1318this.registerDisposable(this.eventHandler_);13191320/**1321* The current element being dragged. This is needed because a DragDropItem1322* can have multiple elements that can be dragged.1323* @private {?Element}1324*/1325this.currentDragElement_ = null;13261327/** @private {?goog.math.Coordinate} */1328this.startPosition_;13291330if (!this.element) {1331throw Error('Invalid argument');1332}1333};1334goog.inherits(goog.fx.DragDropItem, goog.events.EventTarget);133513361337/**1338* Get the data associated with the source/target.1339* @return {Object|null|undefined} Data associated with the source/target.1340*/1341goog.fx.DragDropItem.prototype.getData = function() {1342return this.data;1343};134413451346/**1347* Gets the element that is actually draggable given that the given target was1348* attempted to be dragged. This should be overriden when the element that was1349* given actually contains many items that can be dragged. From the target, you1350* can determine what element should actually be dragged.1351*1352* @param {Element} target The target that was attempted to be dragged.1353* @return {Element} The element that is draggable given the target. If1354* none are draggable, this will return null.1355*/1356goog.fx.DragDropItem.prototype.getDraggableElement = function(target) {1357return target;1358};135913601361/**1362* Gets the element that is currently being dragged.1363*1364* @return {Element} The element that is currently being dragged.1365*/1366goog.fx.DragDropItem.prototype.getCurrentDragElement = function() {1367return this.currentDragElement_;1368};136913701371/**1372* Gets all the elements of this item that are potentially draggable/1373*1374* @return {!Array<Element>} The draggable elements.1375*/1376goog.fx.DragDropItem.prototype.getDraggableElements = function() {1377return [this.element];1378};137913801381/**1382* Event handler for mouse down.1383*1384* @param {goog.events.BrowserEvent} event Mouse down event.1385* @private1386*/1387goog.fx.DragDropItem.prototype.mouseDown_ = function(event) {1388if (!event.isMouseActionButton()) {1389return;1390}13911392// Get the draggable element for the target.1393var element = this.getDraggableElement(/** @type {Element} */ (event.target));1394if (element) {1395this.maybeStartDrag_(event, element);1396}1397};139813991400/**1401* Sets the dragdrop to which this item belongs.1402* @param {goog.fx.AbstractDragDrop} parent The parent dragdrop.1403*/1404goog.fx.DragDropItem.prototype.setParent = function(parent) {1405this.parent_ = parent;1406};140714081409/**1410* Adds mouse move, mouse out and mouse up handlers.1411*1412* @param {goog.events.BrowserEvent} event Mouse down event.1413* @param {Element} element Element.1414* @private1415*/1416goog.fx.DragDropItem.prototype.maybeStartDrag_ = function(event, element) {1417var eventType = goog.events.EventType;1418this.eventHandler_1419.listen(element, eventType.MOUSEMOVE, this.mouseMove_, false)1420.listen(element, eventType.MOUSEOUT, this.mouseMove_, false);14211422// Capture the MOUSEUP on the document to ensure that we cancel the start1423// drag handlers even if the mouse up occurs on some other element. This can1424// happen for instance when the mouse down changes the geometry of the element1425// clicked on (e.g. through changes in activation styling) such that the mouse1426// up occurs outside the original element.1427var doc = goog.dom.getOwnerDocument(element);1428this.eventHandler_.listen(doc, eventType.MOUSEUP, this.mouseUp_, true);14291430this.currentDragElement_ = element;14311432this.startPosition_ = new goog.math.Coordinate(event.clientX, event.clientY);1433};143414351436/**1437* Event handler for mouse move. Starts drag operation if moved more than the1438* threshold value.1439*1440* @param {goog.events.BrowserEvent} event Mouse move or mouse out event.1441* @private1442*/1443goog.fx.DragDropItem.prototype.mouseMove_ = function(event) {1444var distance = Math.abs(event.clientX - this.startPosition_.x) +1445Math.abs(event.clientY - this.startPosition_.y);1446// Fire dragStart event if the drag distance exceeds the threshold or if the1447// mouse leave the dragged element.1448// TODO(user): Consider using the goog.fx.Dragger to track the distance1449// even after the mouse leaves the dragged element.1450var currentDragElement = this.currentDragElement_;1451var distanceAboveThreshold =1452distance > goog.fx.AbstractDragDrop.initDragDistanceThreshold;1453var mouseOutOnDragElement = event.type == goog.events.EventType.MOUSEOUT &&1454event.target == currentDragElement;1455if (distanceAboveThreshold || mouseOutOnDragElement) {1456this.eventHandler_.removeAll();1457this.parent_.startDrag(event, this);1458}14591460// Prevent text selection while dragging an element.1461event.preventDefault();1462};146314641465/**1466* Event handler for mouse up. Removes mouse move, mouse out and mouse up event1467* handlers.1468*1469* @param {goog.events.BrowserEvent} event Mouse up event.1470* @private1471*/1472goog.fx.DragDropItem.prototype.mouseUp_ = function(event) {1473this.eventHandler_.removeAll();1474delete this.startPosition_;1475this.currentDragElement_ = null;1476};1477147814791480/**1481* Class representing an active drop target1482*1483* @param {goog.math.Box} box Box describing the position and dimension of the1484* target item.1485* @param {goog.fx.AbstractDragDrop=} opt_target Target that contains the item1486associated with position.1487* @param {goog.fx.DragDropItem=} opt_item Item associated with position.1488* @param {Element=} opt_element Element of item associated with position.1489* @constructor1490* @struct1491* @private1492*/1493goog.fx.ActiveDropTarget_ = function(box, opt_target, opt_item, opt_element) {14941495/**1496* Box describing the position and dimension of the target item1497* @type {goog.math.Box}1498* @private1499*/1500this.box_ = box;15011502/**1503* Target that contains the item associated with position1504* @type {goog.fx.AbstractDragDrop|undefined}1505* @private1506*/1507this.target_ = opt_target;15081509/**1510* Item associated with position1511* @type {goog.fx.DragDropItem|undefined}1512* @private1513*/1514this.item_ = opt_item;15151516/**1517* The draggable element of the item associated with position.1518* @type {Element}1519* @private1520*/1521this.element_ = opt_element || null;15221523/**1524* If this target is in a scrollable container this is it.1525* @private {?goog.fx.ScrollableContainer_}1526*/1527this.scrollableContainer_ = null;1528};1529153015311532/**1533* Class for representing a scrollable container1534* @param {Element} element the scrollable element.1535* @constructor1536* @private1537*/1538goog.fx.ScrollableContainer_ = function(element) {15391540/**1541* The targets that lie within this container.1542* @type {Array<goog.fx.ActiveDropTarget_>}1543* @private1544*/1545this.containedTargets_ = [];15461547/**1548* The element that is this container1549* @type {Element}1550* @private1551*/1552this.element_ = element;15531554/**1555* The saved scroll left location for calculating deltas.1556* @type {number}1557* @private1558*/1559this.savedScrollLeft_ = 0;15601561/**1562* The saved scroll top location for calculating deltas.1563* @type {number}1564* @private1565*/1566this.savedScrollTop_ = 0;15671568/**1569* The space occupied by the container.1570* @type {goog.math.Box}1571* @private1572*/1573this.box_ = null;1574};157515761577