Path: blob/trunk/third_party/closure/goog/fx/abstractdragdrop.js
4209 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview Abstract Base Class for Drag and Drop.8*9* Provides functionality for implementing drag and drop classes. Also provides10* support classes and events.11*/1213goog.provide('goog.fx.AbstractDragDrop');14goog.provide('goog.fx.AbstractDragDrop.EventType');15goog.provide('goog.fx.DragDropEvent');16goog.provide('goog.fx.DragDropItem');1718goog.require('goog.array');19goog.require('goog.asserts');20goog.require('goog.dom');21goog.require('goog.dom.classlist');22goog.require('goog.events');23goog.require('goog.events.Event');24goog.require('goog.events.EventHandler');25goog.require('goog.events.EventTarget');26goog.require('goog.events.EventType');27goog.require('goog.fx.Dragger');28goog.require('goog.math.Box');29goog.require('goog.math.Coordinate');30goog.require('goog.style');31goog.requireType('goog.events.BrowserEvent');32goog.requireType('goog.fx.DragEvent');33343536/**37* Abstract class that provides reusable functionality for implementing drag38* and drop functionality.39*40* This class also allows clients to define their own subtargeting function41* so that drop areas can have finer granularity than a single element. This is42* accomplished by using a client provided function to map from element and43* coordinates to a subregion id.44*45* This class can also be made aware of scrollable containers that contain46* drop targets by calling addScrollableContainer. This will cause dnd to47* take changing scroll positions into account while a drag is occurring.48*49* @extends {goog.events.EventTarget}50* @constructor51* @struct52*/53goog.fx.AbstractDragDrop = function() {54'use strict';55goog.fx.AbstractDragDrop.base(this, 'constructor');5657/**58* List of items that makes up the drag source or drop target.59* @protected {Array<goog.fx.DragDropItem>}60* @suppress {underscore|visibility}61*/62this.items_ = [];6364/**65* List of associated drop targets.66* @private {Array<goog.fx.AbstractDragDrop>}67*/68this.targets_ = [];6970/**71* Scrollable containers to account for during drag72* @private {Array<goog.fx.ScrollableContainer_>}73*/74this.scrollableContainers_ = [];7576/**77* Flag indicating if it's a drag source, set by addTarget.78* @private {boolean}79*/80this.isSource_ = false;8182/**83* Flag indicating if it's a drop target, set when added as target to another84* DragDrop object.85* @private {boolean}86*/87this.isTarget_ = false;8889/**90* Subtargeting function accepting args:91* (goog.fx.DragDropItem, goog.math.Box, number, number)92* @private {?Function}93*/94this.subtargetFunction_;9596/**97* Last active subtarget.98* @private {?Object}99*/100this.activeSubtarget_;101102/**103* Class name to add to source elements being dragged. Set by setDragClass.104* @private {?string}105*/106this.dragClass_;107108/**109* Class name to add to source elements. Set by setSourceClass.110* @private {?string}111*/112this.sourceClass_;113114/**115* Class name to add to target elements. Set by setTargetClass.116* @private {?string}117*/118this.targetClass_;119120/**121* The SCROLL event target used to make drag element follow scrolling.122* @private {?EventTarget}123*/124this.scrollTarget_;125126/**127* Dummy target, {@see maybeCreateDummyTargetForPosition_}.128* @private {?goog.fx.ActiveDropTarget_}129*/130this.dummyTarget_;131132/**133* Whether the object has been initialized.134* @private {boolean}135*/136this.initialized_ = false;137138/** @private {?Element} */139this.dragEl_;140141/** @private {?Array<!goog.fx.ActiveDropTarget_>} */142this.targetList_;143144/** @private {?goog.math.Box} */145this.targetBox_;146147/** @private {?goog.fx.ActiveDropTarget_} */148this.activeTarget_;149150/** @private {?goog.fx.DragDropItem} */151this.dragItem_;152153/** @private {?goog.fx.Dragger} */154this.dragger_;155};156goog.inherits(goog.fx.AbstractDragDrop, goog.events.EventTarget);157158159/**160* Minimum size (in pixels) for a dummy target. If the box for the target is161* less than the specified size it's not created.162* @const {number}163* @private164*/165goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ = 10;166167168/**169* Constants for event names170* @const171*/172goog.fx.AbstractDragDrop.EventType = {173/** @const */174DRAGOVER: 'dragover',175/** @const */176DRAGOUT: 'dragout',177/** @const */178DRAG: 'drag',179/** @const */180DROP: 'drop',181/** @const */182DRAGSTART: 'dragstart',183/** @const */184DRAGEND: 'dragend'185};186187188/**189* Constant for distance threshold, in pixels, an element has to be moved to190* initiate a drag operation.191* @type {number}192*/193goog.fx.AbstractDragDrop.initDragDistanceThreshold = 5;194195196/**197* Set class to add to source elements being dragged.198*199* @param {string} className Class to be added. Must be a single, valid200* classname.201*/202goog.fx.AbstractDragDrop.prototype.setDragClass = function(className) {203'use strict';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) {215'use strict';216this.sourceClass_ = className;217};218219220/**221* Set class to add to target elements.222*223* @param {string} className Class to be added. Must be a single, valid224* classname.225*/226goog.fx.AbstractDragDrop.prototype.setTargetClass = function(className) {227'use strict';228this.targetClass_ = className;229};230231232/**233* Whether the control has been initialized.234*235* @return {boolean} True if it's been initialized.236*/237goog.fx.AbstractDragDrop.prototype.isInitialized = function() {238'use strict';239return this.initialized_;240};241242243/**244* Add item to drag object.245*246* @param {Element|string} element Dom Node, or string representation of node247* id, to be used as drag source/drop target.248* @throws Error Thrown if called on instance of abstract class249*/250goog.fx.AbstractDragDrop.prototype.addItem = goog.abstractMethod;251252253/**254* Associate drop target with drag element.255*256* @param {goog.fx.AbstractDragDrop} target Target to add.257*/258goog.fx.AbstractDragDrop.prototype.addTarget = function(target) {259'use strict';260this.targets_.push(target);261target.isTarget_ = true;262this.isSource_ = true;263};264265266/**267* Removes the specified target from the list of drop targets.268*269* @param {!goog.fx.AbstractDragDrop} target Target to remove.270*/271goog.fx.AbstractDragDrop.prototype.removeTarget = function(target) {272'use strict';273goog.array.remove(this.targets_, target);274if (this.activeTarget_ && this.activeTarget_.target_ == target) {275this.activeTarget_ = null;276}277this.recalculateDragTargets();278};279280281/**282* Sets the SCROLL event target to make drag element follow scrolling.283*284* @param {EventTarget} scrollTarget The element that dispatches SCROLL events.285*/286goog.fx.AbstractDragDrop.prototype.setScrollTarget = function(scrollTarget) {287'use strict';288this.scrollTarget_ = scrollTarget;289};290291292/**293* Initialize drag and drop functionality for sources/targets already added.294* Sources/targets added after init has been called will initialize themselves295* one by one.296*/297goog.fx.AbstractDragDrop.prototype.init = function() {298'use strict';299if (this.initialized_) {300return;301}302for (var item, i = 0; item = this.items_[i]; i++) {303this.initItem(item);304}305306this.initialized_ = true;307};308309310/**311* Initializes a single item.312*313* @param {goog.fx.DragDropItem} item Item to initialize.314* @protected315*/316goog.fx.AbstractDragDrop.prototype.initItem = function(item) {317'use strict';318if (this.isSource_) {319goog.events.listen(320item.element, goog.events.EventType.MOUSEDOWN, item.mouseDown_, false,321item);322if (this.sourceClass_) {323goog.dom.classlist.add(324goog.asserts.assert(item.element), this.sourceClass_);325}326}327328if (this.isTarget_ && this.targetClass_) {329goog.dom.classlist.add(330goog.asserts.assert(item.element), this.targetClass_);331}332};333334335/**336* Called when removing an item. Removes event listeners and classes.337*338* @param {goog.fx.DragDropItem} item Item to dispose.339* @protected340*/341goog.fx.AbstractDragDrop.prototype.disposeItem = function(item) {342'use strict';343if (this.isSource_) {344goog.events.unlisten(345item.element, goog.events.EventType.MOUSEDOWN, item.mouseDown_, false,346item);347if (this.sourceClass_) {348goog.dom.classlist.remove(349goog.asserts.assert(item.element), this.sourceClass_);350}351}352if (this.isTarget_ && this.targetClass_) {353goog.dom.classlist.remove(354goog.asserts.assert(item.element), this.targetClass_);355}356item.dispose();357};358359360/**361* Removes all items.362*/363goog.fx.AbstractDragDrop.prototype.removeItems = function() {364'use strict';365for (var item, i = 0; item = this.items_[i]; i++) {366this.disposeItem(item);367}368this.items_.length = 0;369};370371372/**373* Starts a drag event for an item if the mouse button stays pressed and the374* cursor moves a few pixels. Allows dragging of items without first having to375* register them with addItem.376*377* @param {goog.events.BrowserEvent} event Mouse down event.378* @param {goog.fx.DragDropItem} item Item that's being dragged.379*/380goog.fx.AbstractDragDrop.prototype.maybeStartDrag = function(event, item) {381'use strict';382item.maybeStartDrag_(event, item.element);383};384385386/**387* Event handler that's used to start drag.388*389* @param {goog.events.BrowserEvent} event Mouse move event.390* @param {goog.fx.DragDropItem} item Item that's being dragged.391*/392goog.fx.AbstractDragDrop.prototype.startDrag = function(event, item) {393'use strict';394// Prevent a new drag operation from being started if another one is already395// in progress (could happen if the mouse was released outside of the396// document).397if (this.dragItem_) {398return;399}400401this.dragItem_ = item;402403// Dispatch DRAGSTART event404var dragStartEvent = new goog.fx.DragDropEvent(405goog.fx.AbstractDragDrop.EventType.DRAGSTART, this, this.dragItem_,406undefined, // opt_target407undefined, // opt_targetItem408undefined, // opt_targetElement409undefined, // opt_clientX410undefined, // opt_clientY411undefined, // opt_x412undefined, // opt_y413undefined, // opt_subtarget414event);415if (this.dispatchEvent(dragStartEvent) == false) {416this.dragItem_ = null;417return;418}419420// Get the source element and create a drag element for it.421var el = item.getCurrentDragElement();422this.dragEl_ = this.createDragElement(el);423var doc = goog.dom.getOwnerDocument(el);424doc.body.appendChild(/** @type {!Node} */ (this.dragEl_));425426this.dragger_ = this.createDraggerFor(el, this.dragEl_, event);427this.dragger_.setScrollTarget(this.scrollTarget_);428429goog.events.listen(430this.dragger_, goog.fx.Dragger.EventType.DRAG, this.moveDrag_, false,431this);432433goog.events.listen(434this.dragger_, goog.fx.Dragger.EventType.END, this.endDrag, false, this);435436// IE may issue a 'selectstart' event when dragging over an iframe even when437// default mousemove behavior is suppressed. If the default selectstart438// behavior is not suppressed, elements dragged over will show as selected.439goog.events.listen(440doc.body, goog.events.EventType.SELECTSTART, this.suppressSelect_);441442this.recalculateDragTargets();443this.recalculateScrollableContainers();444this.activeTarget_ = null;445this.initScrollableContainerListeners_();446this.dragger_.startDrag(event);447448event.preventDefault();449};450451452/**453* Recalculates the geometry of this source's drag targets. Call this454* if the position or visibility of a drag target has changed during455* a drag, or if targets are added or removed.456*457* TODO(user): this is an expensive operation; more efficient APIs458* may be necessary.459*/460goog.fx.AbstractDragDrop.prototype.recalculateDragTargets = function() {461'use strict';462this.targetList_ = [];463for (var target, i = 0; target = this.targets_[i]; i++) {464for (var itm, j = 0; itm = target.items_[j]; j++) {465this.addDragTarget_(target, itm);466}467}468if (!this.targetBox_) {469this.targetBox_ = new goog.math.Box(0, 0, 0, 0);470}471};472473474/**475* Recalculates the current scroll positions of scrollable containers and476* allocates targets. Call this if the position of a container changed or if477* targets are added or removed.478*/479goog.fx.AbstractDragDrop.prototype.recalculateScrollableContainers =480function() {481'use strict';482var container, i, j, target;483for (i = 0; container = this.scrollableContainers_[i]; i++) {484container.containedTargets_ = [];485container.savedScrollLeft_ = container.element_.scrollLeft;486container.savedScrollTop_ = container.element_.scrollTop;487var pos = goog.style.getPageOffset(container.element_);488var size = goog.style.getSize(container.element_);489container.box_ = new goog.math.Box(490pos.y, pos.x + size.width, pos.y + size.height, pos.x);491}492493for (i = 0; target = this.targetList_[i]; i++) {494for (j = 0; container = this.scrollableContainers_[j]; j++) {495if (goog.dom.contains(container.element_, target.element_)) {496container.containedTargets_.push(target);497target.scrollableContainer_ = container;498}499}500}501};502503504/**505* Creates the Dragger for the drag element.506* @param {Element} sourceEl Drag source element.507* @param {Element} el the element created by createDragElement().508* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.509* @return {!goog.fx.Dragger} The new Dragger.510* @protected511*/512goog.fx.AbstractDragDrop.prototype.createDraggerFor = function(513sourceEl, el, event) {514'use strict';515// Position the drag element.516var pos = this.getDragElementPosition(sourceEl, el, event);517el.style.position = 'absolute';518el.style.left = pos.x + 'px';519el.style.top = pos.y + 'px';520return new goog.fx.Dragger(el);521};522523524/**525* Event handler that's used to stop drag. Fires a drop event if over a valid526* target.527*528* @param {goog.fx.DragEvent} event Drag event.529*/530goog.fx.AbstractDragDrop.prototype.endDrag = function(event) {531'use strict';532var activeTarget = event.dragCanceled ? null : this.activeTarget_;533if (activeTarget && activeTarget.target_) {534var clientX = event.clientX;535var clientY = event.clientY;536var scroll = this.getScrollPos();537var x = clientX + scroll.x;538var y = clientY + scroll.y;539540var subtarget;541// If a subtargeting function is enabled get the current subtarget542if (this.subtargetFunction_) {543subtarget =544this.subtargetFunction_(activeTarget.item_, activeTarget.box_, x, y);545}546547var dragEvent = new goog.fx.DragDropEvent(548goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,549activeTarget.target_, activeTarget.item_, activeTarget.element_,550clientX, clientY, x, y);551this.dispatchEvent(dragEvent);552553var dropEvent = new goog.fx.DragDropEvent(554goog.fx.AbstractDragDrop.EventType.DROP, this, this.dragItem_,555activeTarget.target_, activeTarget.item_, activeTarget.element_,556clientX, clientY, x, y, subtarget, event.browserEvent);557activeTarget.target_.dispatchEvent(dropEvent);558}559560var dragEndEvent = new goog.fx.DragDropEvent(561goog.fx.AbstractDragDrop.EventType.DRAGEND, this, this.dragItem_,562activeTarget ? activeTarget.target_ : undefined,563activeTarget ? activeTarget.item_ : undefined,564activeTarget ? activeTarget.element_ : undefined);565this.dispatchEvent(dragEndEvent);566567goog.events.unlisten(568this.dragger_, goog.fx.Dragger.EventType.DRAG, this.moveDrag_, false,569this);570goog.events.unlisten(571this.dragger_, goog.fx.Dragger.EventType.END, this.endDrag, false, this);572var doc = goog.dom.getOwnerDocument(this.dragItem_.getCurrentDragElement());573goog.events.unlisten(574doc.body, goog.events.EventType.SELECTSTART, this.suppressSelect_);575576577this.afterEndDrag(this.activeTarget_ ? this.activeTarget_.item_ : null);578};579580581/**582* Called after a drag operation has finished.583*584* @param {goog.fx.DragDropItem=} opt_dropTarget Target for successful drop.585* @protected586*/587goog.fx.AbstractDragDrop.prototype.afterEndDrag = function(opt_dropTarget) {588'use strict';589this.disposeDrag();590};591592593/**594* Called once a drag operation has finished. Removes event listeners and595* elements.596*597* @protected598*/599goog.fx.AbstractDragDrop.prototype.disposeDrag = function() {600'use strict';601this.disposeScrollableContainerListeners_();602this.dragger_.dispose();603604goog.dom.removeNode(this.dragEl_);605delete this.dragItem_;606delete this.dragEl_;607delete this.dragger_;608delete this.targetList_;609delete this.activeTarget_;610};611612613/**614* Event handler for drag events. Determines the active drop target, if any, and615* fires dragover and dragout events appropriately.616*617* @param {goog.fx.DragEvent} event Drag event.618* @private619*/620goog.fx.AbstractDragDrop.prototype.moveDrag_ = function(event) {621'use strict';622var position = this.getEventPosition(event);623var x = position.x;624var y = position.y;625626var activeTarget = this.activeTarget_;627628this.dispatchEvent(new goog.fx.DragDropEvent(629goog.fx.AbstractDragDrop.EventType.DRAG, this, this.dragItem_,630activeTarget ? activeTarget.target_ : undefined,631activeTarget ? activeTarget.item_ : undefined,632activeTarget ? activeTarget.element_ : undefined, event.clientX,633event.clientY, x, y));634635// Check if we're still inside the bounds of the active target, if not fire636// a dragout event and proceed to find a new target.637var subtarget;638if (activeTarget) {639// If a subtargeting function is enabled get the current subtarget640if (this.subtargetFunction_ && activeTarget.target_) {641subtarget =642this.subtargetFunction_(activeTarget.item_, activeTarget.box_, x, y);643}644645if (activeTarget.box_.contains(position) &&646subtarget == this.activeSubtarget_) {647return;648}649650if (activeTarget.target_) {651var sourceDragOutEvent = new goog.fx.DragDropEvent(652goog.fx.AbstractDragDrop.EventType.DRAGOUT, this, this.dragItem_,653activeTarget.target_, activeTarget.item_, activeTarget.element_);654this.dispatchEvent(sourceDragOutEvent);655656// The event should be dispatched the by target DragDrop so that the657// target DragDrop can manage these events without having to know what658// sources this is a target for.659var targetDragOutEvent = new goog.fx.DragDropEvent(660goog.fx.AbstractDragDrop.EventType.DRAGOUT, this, this.dragItem_,661activeTarget.target_, activeTarget.item_, activeTarget.element_,662undefined, undefined, undefined, undefined, this.activeSubtarget_);663activeTarget.target_.dispatchEvent(targetDragOutEvent);664}665this.activeSubtarget_ = subtarget;666this.activeTarget_ = null;667}668669// Check if inside target box670if (this.targetBox_.contains(position)) {671// Search for target and fire a dragover event if found672activeTarget = this.activeTarget_ = this.getTargetFromPosition_(position);673if (activeTarget && activeTarget.target_) {674// If a subtargeting function is enabled get the current subtarget675if (this.subtargetFunction_) {676subtarget = this.subtargetFunction_(677activeTarget.item_, activeTarget.box_, x, y);678}679var sourceDragOverEvent = new goog.fx.DragDropEvent(680goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,681activeTarget.target_, activeTarget.item_, activeTarget.element_);682sourceDragOverEvent.subtarget = subtarget;683this.dispatchEvent(sourceDragOverEvent);684685// The event should be dispatched by the target DragDrop so that the686// target DragDrop can manage these events without having to know what687// sources this is a target for.688var targetDragOverEvent = new goog.fx.DragDropEvent(689goog.fx.AbstractDragDrop.EventType.DRAGOVER, this, this.dragItem_,690activeTarget.target_, activeTarget.item_, activeTarget.element_,691event.clientX, event.clientY, undefined, undefined, subtarget);692activeTarget.target_.dispatchEvent(targetDragOverEvent);693694} else if (!activeTarget) {695// If no target was found create a dummy one so we won't have to iterate696// over all possible targets for every move event.697this.activeTarget_ = this.maybeCreateDummyTargetForPosition_(x, y);698}699}700};701702703/**704* Event handler for suppressing selectstart events. Selecting should be705* disabled while dragging.706*707* @param {goog.events.Event} event The selectstart event to suppress.708* @return {boolean} Whether to perform default behavior.709* @private710*/711goog.fx.AbstractDragDrop.prototype.suppressSelect_ = function(event) {712'use strict';713return false;714};715716717/**718* Sets up listeners for the scrollable containers that keep track of their719* scroll positions.720* @private721*/722goog.fx.AbstractDragDrop.prototype.initScrollableContainerListeners_ =723function() {724'use strict';725var container, i;726for (i = 0; container = this.scrollableContainers_[i]; i++) {727goog.events.listen(728container.element_, goog.events.EventType.SCROLL,729this.containerScrollHandler_, false, this);730}731};732733734/**735* Cleans up the scrollable container listeners.736* @private737*/738goog.fx.AbstractDragDrop.prototype.disposeScrollableContainerListeners_ =739function() {740'use strict';741for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {742goog.events.unlisten(743container.element_, 'scroll', this.containerScrollHandler_, false,744this);745container.containedTargets_ = [];746}747};748749750/**751* Makes drag and drop aware of a target container that could scroll mid drag.752* @param {Element} element The scroll container.753*/754goog.fx.AbstractDragDrop.prototype.addScrollableContainer = function(element) {755'use strict';756this.scrollableContainers_.push(new goog.fx.ScrollableContainer_(element));757};758759760/**761* Removes all scrollable containers.762*/763goog.fx.AbstractDragDrop.prototype.removeAllScrollableContainers = function() {764'use strict';765this.disposeScrollableContainerListeners_();766this.scrollableContainers_ = [];767};768769770/**771* Event handler for containers scrolling.772* @param {goog.events.BrowserEvent} e The event.773* @suppress {visibility} TODO(martone): update dependent projects.774* @private775*/776goog.fx.AbstractDragDrop.prototype.containerScrollHandler_ = function(e) {777'use strict';778for (var i = 0, container; container = this.scrollableContainers_[i]; i++) {779if (e.target == container.element_) {780var deltaTop = container.savedScrollTop_ - container.element_.scrollTop;781var deltaLeft =782container.savedScrollLeft_ - container.element_.scrollLeft;783container.savedScrollTop_ = container.element_.scrollTop;784container.savedScrollLeft_ = container.element_.scrollLeft;785786// When the container scrolls, it's possible that one of the targets will787// move to the region contained by the dummy target. Since we don't know788// which sides (if any) of the dummy target are defined by targets789// contained by this container, we are conservative and just shrink it.790if (this.dummyTarget_ && this.activeTarget_ == this.dummyTarget_) {791if (deltaTop > 0) {792this.dummyTarget_.box_.top += deltaTop;793} else {794this.dummyTarget_.box_.bottom += deltaTop;795}796if (deltaLeft > 0) {797this.dummyTarget_.box_.left += deltaLeft;798} else {799this.dummyTarget_.box_.right += deltaLeft;800}801}802for (var j = 0, target; target = container.containedTargets_[j]; j++) {803var box = target.box_;804box.top += deltaTop;805box.left += deltaLeft;806box.bottom += deltaTop;807box.right += deltaLeft;808809this.calculateTargetBox_(box);810}811}812}813this.dragger_.onScroll_(e);814};815816817/**818* Set a function that provides subtargets. A subtargeting function819* returns an arbitrary identifier for each subtarget of an element.820* DnD code will generate additional drag over / out events when821* switching from subtarget to subtarget. This is useful for instance822* if you are interested if you are on the top half or the bottom half823* of the element.824* The provided function will be given the DragDropItem, box, x, y825* box is the current window coordinates occupied by element826* x, y is the mouse position in window coordinates827*828* @param {Function} f The new subtarget function.829*/830goog.fx.AbstractDragDrop.prototype.setSubtargetFunction = function(f) {831'use strict';832this.subtargetFunction_ = f;833};834835836/**837* Creates an element for the item being dragged.838*839* @param {Element} sourceEl Drag source element.840* @return {Element} The new drag element.841*/842goog.fx.AbstractDragDrop.prototype.createDragElement = function(sourceEl) {843'use strict';844var dragEl = this.createDragElementInternal(sourceEl);845goog.asserts.assert(dragEl);846if (this.dragClass_) {847goog.dom.classlist.add(dragEl, this.dragClass_);848}849850return dragEl;851};852853854/**855* Returns the position for the drag element.856*857* @param {Element} el Drag source element.858* @param {Element} dragEl The dragged element created by createDragElement().859* @param {goog.events.BrowserEvent} event Mouse down event for start of drag.860* @return {!goog.math.Coordinate} The position for the drag element.861*/862goog.fx.AbstractDragDrop.prototype.getDragElementPosition = function(863el, dragEl, event) {864'use strict';865var pos = goog.style.getPageOffset(el);866867// Subtract margin from drag element position twice, once to adjust the868// position given by the original node and once for the drag node.869var marginBox = goog.style.getMarginBox(el);870pos.x -= (marginBox.left || 0) * 2;871pos.y -= (marginBox.top || 0) * 2;872873return pos;874};875876877/**878* Returns the dragger object.879*880* @return {goog.fx.Dragger} The dragger object used by this drag and drop881* instance.882*/883goog.fx.AbstractDragDrop.prototype.getDragger = function() {884'use strict';885return this.dragger_;886};887888889/**890* Creates copy of node being dragged.891*892* @param {Element} sourceEl Element to copy.893* @return {!Element} The clone of `sourceEl`.894* @deprecated Use goog.fx.Dragger.cloneNode().895* @private896*/897goog.fx.AbstractDragDrop.prototype.cloneNode_ = function(sourceEl) {898'use strict';899return goog.fx.Dragger.cloneNode(sourceEl);900};901902903/**904* Generates an element to follow the cursor during dragging, given a drag905* source element. The default behavior is simply to clone the source element,906* but this may be overridden in subclasses. This method is called by907* `createDragElement()` before the drag class is added.908*909* @param {Element} sourceEl Drag source element.910* @return {!Element} The new drag element.911* @protected912* @suppress {deprecated}913*/914goog.fx.AbstractDragDrop.prototype.createDragElementInternal = function(915sourceEl) {916'use strict';917return this.cloneNode_(sourceEl);918};919920921/**922* Add possible drop target for current drag operation.923*924* @param {goog.fx.AbstractDragDrop} target Drag handler.925* @param {goog.fx.DragDropItem} item Item that's being dragged.926* @private927*/928goog.fx.AbstractDragDrop.prototype.addDragTarget_ = function(target, item) {929'use strict';930// Get all the draggable elements and add each one.931var draggableElements = item.getDraggableElements();932for (var i = 0; i < draggableElements.length; i++) {933var draggableElement = draggableElements[i];934935// Determine target position and dimension936var box = this.getElementBox(item, draggableElement);937938this.targetList_.push(939new goog.fx.ActiveDropTarget_(box, target, item, draggableElement));940941this.calculateTargetBox_(box);942}943};944945946/**947* Calculates the position and dimension of a draggable element.948*949* @param {goog.fx.DragDropItem} item Item that's being dragged.950* @param {Element} element The element to calculate the box.951*952* @return {!goog.math.Box} Box describing the position and dimension953* of element.954* @protected955*/956goog.fx.AbstractDragDrop.prototype.getElementBox = function(item, element) {957'use strict';958var pos = goog.style.getPageOffset(element);959var size = goog.style.getSize(element);960return new goog.math.Box(961pos.y, pos.x + size.width, pos.y + size.height, pos.x);962};963964965/**966* Calculate the outer bounds (the region all targets are inside).967*968* @param {goog.math.Box} box Box describing the position and dimension969* of a drag target.970* @private971*/972goog.fx.AbstractDragDrop.prototype.calculateTargetBox_ = function(box) {973'use strict';974if (this.targetList_.length == 1) {975this.targetBox_ =976new goog.math.Box(box.top, box.right, box.bottom, box.left);977} else {978var tb = this.targetBox_;979tb.left = Math.min(box.left, tb.left);980tb.right = Math.max(box.right, tb.right);981tb.top = Math.min(box.top, tb.top);982tb.bottom = Math.max(box.bottom, tb.bottom);983}984};985986987/**988* Creates a dummy target for the given cursor position. The assumption is to989* create as big dummy target box as possible, the only constraints are:990* - The dummy target box cannot overlap any of real target boxes.991* - The dummy target has to contain a point with current mouse coordinates.992*993* NOTE: For performance reasons the box construction algorithm is kept simple994* and it is not optimal (see example below). Currently it is O(n) in regard to995* the number of real drop target boxes, but its result depends on the order996* of those boxes being processed (the order in which they're added to the997* targetList_ collection).998*999* The algorithm.1000* a) Assumptions1001* - Mouse pointer is in the bounding box of real target boxes.1002* - None of the boxes have negative coordinate values.1003* - Mouse pointer is not contained by any of "real target" boxes.1004* - For targets inside a scrollable container, the box used is the1005* intersection of the scrollable container's box and the target's box.1006* This is because the part of the target that extends outside the scrollable1007* container should not be used in the clipping calculations.1008*1009* b) Outline1010* - Initialize the fake target to the bounding box of real targets.1011* - For each real target box - clip the fake target box so it does not contain1012* that target box, but does contain the mouse pointer.1013* -- Project the real target box, mouse pointer and fake target box onto1014* both axes and calculate the clipping coordinates.1015* -- Only one coordinate is used to clip the fake target box to keep the1016* fake target as big as possible.1017* -- If the projection of the real target box contains the mouse pointer,1018* clipping for a given axis is not possible.1019* -- If both clippings are possible, the clipping more distant from the1020* mouse pointer is selected to keep bigger fake target area.1021* - Save the created fake target only if it has a big enough area.1022*1023*1024* c) Example1025* <pre>1026* Input: Algorithm created box: Maximum box:1027* +---------------------+ +---------------------+ +---------------------+1028* | B1 | B2 | | B1 B2 | | B1 B2 |1029* | | | | +-------------+ | |+-------------------+|1030* |---------x-----------| | | | | || ||1031* | | | | | | | || ||1032* | | | | | | | || ||1033* | | | | | | | || ||1034* | | | | | | | || ||1035* | | | | +-------------+ | |+-------------------+|1036* | B4 | B3 | | B4 B3 | | B4 B3 |1037* +---------------------+ +---------------------+ +---------------------+1038* </pre>1039*1040* @param {number} x Cursor position on the x-axis.1041* @param {number} y Cursor position on the y-axis.1042* @return {goog.fx.ActiveDropTarget_} Dummy drop target.1043* @private1044*/1045goog.fx.AbstractDragDrop.prototype.maybeCreateDummyTargetForPosition_ =1046function(x, y) {1047'use strict';1048if (!this.dummyTarget_) {1049this.dummyTarget_ = new goog.fx.ActiveDropTarget_(this.targetBox_.clone());1050}1051var fakeTargetBox = this.dummyTarget_.box_;10521053// Initialize the fake target box to the bounding box of DnD targets.1054fakeTargetBox.top = this.targetBox_.top;1055fakeTargetBox.right = this.targetBox_.right;1056fakeTargetBox.bottom = this.targetBox_.bottom;1057fakeTargetBox.left = this.targetBox_.left;10581059// Clip the fake target based on mouse position and DnD target boxes.1060for (var i = 0, target; target = this.targetList_[i]; i++) {1061var box = target.box_;10621063if (target.scrollableContainer_) {1064// If the target has a scrollable container, use the intersection of that1065// container's box and the target's box.1066var scrollBox = target.scrollableContainer_.box_;10671068box = new goog.math.Box(1069Math.max(box.top, scrollBox.top),1070Math.min(box.right, scrollBox.right),1071Math.min(box.bottom, scrollBox.bottom),1072Math.max(box.left, scrollBox.left));1073}10741075// Calculate clipping coordinates for horizontal and vertical axis.1076// The clipping coordinate is calculated by projecting fake target box,1077// the mouse pointer and DnD target box onto an axis and checking how1078// box projections overlap and if the projected DnD target box contains1079// mouse pointer. The clipping coordinate cannot be computed and is set to1080// a negative value if the projected DnD target contains the mouse pointer.10811082var horizontalClip = null; // Assume mouse is above or below the DnD box.1083if (x >= box.right) { // Mouse is to the right of the DnD box.1084// Clip the fake box only if the DnD box overlaps it.1085horizontalClip =1086box.right > fakeTargetBox.left ? box.right : fakeTargetBox.left;1087} else if (x < box.left) { // Mouse is to the left of the DnD box.1088// Clip the fake box only if the DnD box overlaps it.1089horizontalClip =1090box.left < fakeTargetBox.right ? box.left : fakeTargetBox.right;1091}1092var verticalClip = null;1093if (y >= box.bottom) {1094verticalClip =1095box.bottom > fakeTargetBox.top ? box.bottom : fakeTargetBox.top;1096} else if (y < box.top) {1097verticalClip =1098box.top < fakeTargetBox.bottom ? box.top : fakeTargetBox.bottom;1099}11001101// If both clippings are possible, choose one that gives us larger distance1102// to mouse pointer (mark the shorter clipping as impossible, by setting it1103// to null).1104if (horizontalClip !== null && verticalClip !== null) {1105if (Math.abs(horizontalClip - x) > Math.abs(verticalClip - y)) {1106verticalClip = null;1107} else {1108horizontalClip = null;1109}1110}11111112// Clip none or one of fake target box sides (at most one clipping1113// coordinate can be active).1114if (horizontalClip !== null) {1115if (horizontalClip <= x) {1116fakeTargetBox.left = horizontalClip;1117} else {1118fakeTargetBox.right = horizontalClip;1119}1120} else if (verticalClip !== null) {1121if (verticalClip <= y) {1122fakeTargetBox.top = verticalClip;1123} else {1124fakeTargetBox.bottom = verticalClip;1125}1126}1127}11281129// Only return the new fake target if it is big enough.1130return (fakeTargetBox.right - fakeTargetBox.left) *1131(fakeTargetBox.bottom - fakeTargetBox.top) >=1132goog.fx.AbstractDragDrop.DUMMY_TARGET_MIN_SIZE_ ?1133this.dummyTarget_ :1134null;1135};113611371138/**1139* Returns the target for a given cursor position.1140*1141* @param {goog.math.Coordinate} position Cursor position.1142* @return {goog.fx.ActiveDropTarget_} Target for position or null if no target1143* was defined for the given position.1144* @private1145*/1146goog.fx.AbstractDragDrop.prototype.getTargetFromPosition_ = function(position) {1147'use strict';1148for (var target, i = 0; target = this.targetList_[i]; i++) {1149if (target.box_.contains(position)) {1150if (target.scrollableContainer_) {1151// If we have a scrollable container we will need to make sure1152// we account for clipping of the scroll area1153var box = target.scrollableContainer_.box_;1154if (box.contains(position)) {1155return target;1156}1157} else {1158return target;1159}1160}1161}11621163return null;1164};116511661167/**1168* Checks whatever a given point is inside a given box.1169*1170* @param {number} x Cursor position on the x-axis.1171* @param {number} y Cursor position on the y-axis.1172* @param {goog.math.Box} box Box to check position against.1173* @return {boolean} Whether the given point is inside `box`.1174* @protected1175* @deprecated Use goog.math.Box.contains.1176*/1177goog.fx.AbstractDragDrop.prototype.isInside = function(x, y, box) {1178'use strict';1179return x >= box.left && x < box.right && y >= box.top && y < box.bottom;1180};118111821183/**1184* Gets the scroll distance as a coordinate object, using1185* the window of the current drag element's dom.1186* @return {!goog.math.Coordinate} Object with scroll offsets 'x' and 'y'.1187* @protected1188*/1189goog.fx.AbstractDragDrop.prototype.getScrollPos = function() {1190'use strict';1191return goog.dom.getDomHelper(this.dragEl_).getDocumentScroll();1192};119311941195/**1196* Get the position of a drag event.1197* @param {goog.fx.DragEvent} event Drag event.1198* @return {!goog.math.Coordinate} Position of the event.1199* @protected1200*/1201goog.fx.AbstractDragDrop.prototype.getEventPosition = function(event) {1202'use strict';1203var scroll = this.getScrollPos();1204return new goog.math.Coordinate(1205event.clientX + scroll.x, event.clientY + scroll.y);1206};120712081209/**1210* @override1211* @protected1212*/1213goog.fx.AbstractDragDrop.prototype.disposeInternal = function() {1214'use strict';1215goog.fx.AbstractDragDrop.base(this, 'disposeInternal');1216this.removeItems();1217};1218121912201221/**1222* Object representing a drag and drop event.1223*1224* @param {string} type Event type.1225* @param {goog.fx.AbstractDragDrop} source Source drag drop object.1226* @param {goog.fx.DragDropItem} sourceItem Source item.1227* @param {goog.fx.AbstractDragDrop=} opt_target Target drag drop object.1228* @param {goog.fx.DragDropItem=} opt_targetItem Target item.1229* @param {Element=} opt_targetElement Target element.1230* @param {number=} opt_clientX X-Position relative to the screen.1231* @param {number=} opt_clientY Y-Position relative to the screen.1232* @param {number=} opt_x X-Position relative to the viewport.1233* @param {number=} opt_y Y-Position relative to the viewport.1234* @param {Object=} opt_subtarget The currently active subtarget.1235* @param {goog.events.BrowserEvent=} opt_browserEvent The browser event1236* that caused this dragdrop event.1237* @extends {goog.events.Event}1238* @constructor1239* @struct1240*/1241goog.fx.DragDropEvent = function(1242type, source, sourceItem, opt_target, opt_targetItem, opt_targetElement,1243opt_clientX, opt_clientY, opt_x, opt_y, opt_subtarget, opt_browserEvent) {1244'use strict';1245// TODO(eae): Get rid of all the optional parameters and have the caller set1246// the fields directly instead.1247goog.fx.DragDropEvent.base(this, 'constructor', type);12481249/**1250* Reference to the source goog.fx.AbstractDragDrop object.1251* @type {goog.fx.AbstractDragDrop}1252*/1253this.dragSource = source;12541255/**1256* Reference to the source goog.fx.DragDropItem object.1257* @type {goog.fx.DragDropItem}1258*/1259this.dragSourceItem = sourceItem;12601261/**1262* Reference to the target goog.fx.AbstractDragDrop object.1263* @type {goog.fx.AbstractDragDrop|undefined}1264*/1265this.dropTarget = opt_target;12661267/**1268* Reference to the target goog.fx.DragDropItem object.1269* @type {goog.fx.DragDropItem|undefined}1270*/1271this.dropTargetItem = opt_targetItem;12721273/**1274* The actual element of the drop target that is the target for this event.1275* @type {Element|undefined}1276*/1277this.dropTargetElement = opt_targetElement;12781279/**1280* X-Position relative to the screen.1281* @type {number|undefined}1282*/1283this.clientX = opt_clientX;12841285/**1286* Y-Position relative to the screen.1287* @type {number|undefined}1288*/1289this.clientY = opt_clientY;12901291/**1292* X-Position relative to the viewport.1293* @type {number|undefined}1294*/1295this.viewportX = opt_x;12961297/**1298* Y-Position relative to the viewport.1299* @type {number|undefined}1300*/1301this.viewportY = opt_y;13021303/**1304* The subtarget that is currently active if a subtargeting function1305* is supplied.1306* @type {Object|undefined}1307*/1308this.subtarget = opt_subtarget;13091310/**1311* The browser event that caused this dragdrop event.1312* @const1313*/1314this.browserEvent = opt_browserEvent;1315};1316goog.inherits(goog.fx.DragDropEvent, goog.events.Event);1317131813191320/**1321* Class representing a source or target element for drag and drop operations.1322*1323* @param {Element|string} element Dom Node, or string representation of node1324* id, to be used as drag source/drop target.1325* @param {Object=} opt_data Data associated with the source/target.1326* @throws Error If no element argument is provided or if the type is invalid1327* @extends {goog.events.EventTarget}1328* @constructor1329* @struct1330*/1331goog.fx.DragDropItem = function(element, opt_data) {1332'use strict';1333goog.fx.DragDropItem.base(this, 'constructor');13341335/**1336* Reference to drag source/target element1337* @type {Element}1338*/1339this.element = goog.dom.getElement(element);13401341/**1342* Data associated with element.1343* @type {Object|undefined}1344*/1345this.data = opt_data;13461347/**1348* Drag object the item belongs to.1349* @type {goog.fx.AbstractDragDrop?}1350* @private1351*/1352this.parent_ = null;13531354/**1355* Event handler for listeners on events that can initiate a drag.1356* @type {!goog.events.EventHandler<!goog.fx.DragDropItem>}1357* @private1358*/1359this.eventHandler_ = new goog.events.EventHandler(this);1360this.registerDisposable(this.eventHandler_);13611362/**1363* The current element being dragged. This is needed because a DragDropItem1364* can have multiple elements that can be dragged.1365* @private {?Element}1366*/1367this.currentDragElement_ = null;13681369/** @private {?goog.math.Coordinate} */1370this.startPosition_;13711372if (!this.element) {1373throw new Error('Invalid argument');1374}1375};1376goog.inherits(goog.fx.DragDropItem, goog.events.EventTarget);137713781379/**1380* Get the data associated with the source/target.1381* @return {Object|null|undefined} Data associated with the source/target.1382*/1383goog.fx.DragDropItem.prototype.getData = function() {1384'use strict';1385return this.data;1386};138713881389/**1390* Gets the element that is actually draggable given that the given target was1391* attempted to be dragged. This should be overridden when the element that was1392* given actually contains many items that can be dragged. From the target, you1393* can determine what element should actually be dragged.1394*1395* @param {Element} target The target that was attempted to be dragged.1396* @return {Element} The element that is draggable given the target. If1397* none are draggable, this will return null.1398*/1399goog.fx.DragDropItem.prototype.getDraggableElement = function(target) {1400'use strict';1401return target;1402};140314041405/**1406* Gets the element that is currently being dragged.1407*1408* @return {Element} The element that is currently being dragged.1409*/1410goog.fx.DragDropItem.prototype.getCurrentDragElement = function() {1411'use strict';1412return this.currentDragElement_;1413};141414151416/**1417* Gets all the elements of this item that are potentially draggable/1418*1419* @return {!Array<Element>} The draggable elements.1420*/1421goog.fx.DragDropItem.prototype.getDraggableElements = function() {1422'use strict';1423return [this.element];1424};142514261427/**1428* Event handler for mouse down.1429*1430* @param {goog.events.BrowserEvent} event Mouse down event.1431* @private1432*/1433goog.fx.DragDropItem.prototype.mouseDown_ = function(event) {1434'use strict';1435if (!event.isMouseActionButton()) {1436return;1437}14381439// Get the draggable element for the target.1440var element = this.getDraggableElement(/** @type {Element} */ (event.target));1441if (element) {1442this.maybeStartDrag_(event, element);1443}1444};144514461447/**1448* Sets the dragdrop to which this item belongs.1449* @param {goog.fx.AbstractDragDrop} parent The parent dragdrop.1450*/1451goog.fx.DragDropItem.prototype.setParent = function(parent) {1452'use strict';1453this.parent_ = parent;1454};145514561457/**1458* Adds mouse move, mouse out and mouse up handlers.1459*1460* @param {goog.events.BrowserEvent} event Mouse down event.1461* @param {Element} element Element.1462* @private1463*/1464goog.fx.DragDropItem.prototype.maybeStartDrag_ = function(event, element) {1465'use strict';1466var eventType = goog.events.EventType;1467this.eventHandler_1468.listen(element, eventType.MOUSEMOVE, this.mouseMove_, false)1469.listen(element, eventType.MOUSEOUT, this.mouseMove_, false);14701471// Capture the MOUSEUP on the document to ensure that we cancel the start1472// drag handlers even if the mouse up occurs on some other element. This can1473// happen for instance when the mouse down changes the geometry of the element1474// clicked on (e.g. through changes in activation styling) such that the mouse1475// up occurs outside the original element.1476var doc = goog.dom.getOwnerDocument(element);1477this.eventHandler_.listen(doc, eventType.MOUSEUP, this.mouseUp_, true);14781479this.currentDragElement_ = element;14801481this.startPosition_ = new goog.math.Coordinate(event.clientX, event.clientY);1482};148314841485/**1486* Event handler for mouse move. Starts drag operation if moved more than the1487* threshold value.1488*1489* @param {goog.events.BrowserEvent} event Mouse move or mouse out event.1490* @private1491*/1492goog.fx.DragDropItem.prototype.mouseMove_ = function(event) {1493'use strict';1494var distance = Math.abs(event.clientX - this.startPosition_.x) +1495Math.abs(event.clientY - this.startPosition_.y);1496// Fire dragStart event if the drag distance exceeds the threshold or if the1497// mouse leave the dragged element.1498// TODO(user): Consider using the goog.fx.Dragger to track the distance1499// even after the mouse leaves the dragged element.1500var currentDragElement = this.currentDragElement_;1501var distanceAboveThreshold =1502distance > goog.fx.AbstractDragDrop.initDragDistanceThreshold;1503var mouseOutOnDragElement = event.type == goog.events.EventType.MOUSEOUT &&1504event.target == currentDragElement;1505if (distanceAboveThreshold || mouseOutOnDragElement) {1506this.eventHandler_.removeAll();1507this.parent_.startDrag(event, this);1508}15091510// Prevent text selection while dragging an element.1511event.preventDefault();1512};151315141515/**1516* Event handler for mouse up. Removes mouse move, mouse out and mouse up event1517* handlers.1518*1519* @param {goog.events.BrowserEvent} event Mouse up event.1520* @private1521*/1522goog.fx.DragDropItem.prototype.mouseUp_ = function(event) {1523'use strict';1524this.eventHandler_.removeAll();1525delete this.startPosition_;1526this.currentDragElement_ = null;1527};1528152915301531/**1532* Class representing an active drop target1533*1534* @param {goog.math.Box} box Box describing the position and dimension of the1535* target item.1536* @param {goog.fx.AbstractDragDrop=} opt_target Target that contains the item1537associated with position.1538* @param {goog.fx.DragDropItem=} opt_item Item associated with position.1539* @param {Element=} opt_element Element of item associated with position.1540* @constructor1541* @struct1542* @private1543*/1544goog.fx.ActiveDropTarget_ = function(box, opt_target, opt_item, opt_element) {1545'use strict';1546/**1547* Box describing the position and dimension of the target item1548* @type {goog.math.Box}1549* @private1550*/1551this.box_ = box;15521553/**1554* Target that contains the item associated with position1555* @type {goog.fx.AbstractDragDrop|undefined}1556* @private1557*/1558this.target_ = opt_target;15591560/**1561* Item associated with position1562* @type {goog.fx.DragDropItem|undefined}1563* @private1564*/1565this.item_ = opt_item;15661567/**1568* The draggable element of the item associated with position.1569* @type {Element}1570* @private1571*/1572this.element_ = opt_element || null;15731574/**1575* If this target is in a scrollable container this is it.1576* @private {?goog.fx.ScrollableContainer_}1577*/1578this.scrollableContainer_ = null;1579};1580158115821583/**1584* Class for representing a scrollable container1585* @param {Element} element the scrollable element.1586* @constructor1587* @private1588*/1589goog.fx.ScrollableContainer_ = function(element) {1590'use strict';1591/**1592* The targets that lie within this container.1593* @type {Array<goog.fx.ActiveDropTarget_>}1594* @private1595*/1596this.containedTargets_ = [];15971598/**1599* The element that is this container1600* @type {Element}1601* @private1602*/1603this.element_ = element;16041605/**1606* The saved scroll left location for calculating deltas.1607* @type {number}1608* @private1609*/1610this.savedScrollLeft_ = 0;16111612/**1613* The saved scroll top location for calculating deltas.1614* @type {number}1615* @private1616*/1617this.savedScrollTop_ = 0;16181619/**1620* The space occupied by the container.1621* @type {?goog.math.Box}1622* @private1623*/1624this.box_ = null;1625};162616271628/**1629* Test-only exports.1630* @const1631*/1632goog.fx.AbstractDragDrop.TEST_ONLY = {1633ActiveDropTarget: goog.fx.ActiveDropTarget_,1634};163516361637