1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 
  9 /**
 10   @static
 11 */
 12 SC.DRAG_LINK = 0x0004;
 13 
 14 /**
 15   @static
 16 */
 17 SC.DRAG_COPY = 0x0001;
 18 
 19 /**
 20   @static
 21 */
 22 SC.DRAG_MOVE = 0x0002;
 23 
 24 /**
 25   @static
 26 */
 27 SC.DRAG_NONE = 0x0000;
 28 
 29 /**
 30   @static
 31 */
 32 SC.DRAG_ANY = 0x000F;
 33 
 34 /**
 35   @static
 36 */
 37 SC.DRAG_DATA = 0x0008; // includes SC.DRAG_REORDER
 38 
 39 /**
 40   @static
 41 */
 42 SC.DRAG_AUTOSCROLL_ZONE_THICKNESS = 20;
 43 
 44 SC.View.reopen(
 45   /** @scope SC.View.prototype */ {
 46 
 47   /** @private */
 48   init: function (original) {
 49     original();
 50 
 51     // register for drags
 52     if (this.get('isDropTarget')) { SC.Drag.addDropTarget(this); }
 53 
 54     // register scroll views for autoscroll during drags
 55     if (this.get('isScrollable')) { SC.Drag.addScrollableView(this); }
 56   }.enhance(),
 57 
 58   /** @private */
 59   destroy: function (original) {
 60     // unregister for drags
 61     if (this.get('isDropTarget')) { SC.Drag.removeDropTarget(this); }
 62 
 63     // unregister for autoscroll during drags
 64     if (this.get('isScrollable')) { SC.Drag.removeScrollableView(this); }
 65 
 66     return original();
 67   }.enhance()
 68 });
 69 
 70 /**
 71   @class
 72 
 73   An instance of this object is created whenever a drag occurs.  The instance
 74   manages the mouse/touch events and coordinating with droppable targets until the
 75   user releases the mouse button.
 76 
 77   To initiate a drag, you should call `SC.Drag.start()` with the options below
 78   specified in a hash. Pass the ones you need to get the drag you want:
 79 
 80     - `event` -- *(req)* The mouse event/touch that triggered the drag.  This will be used
 81       to position the element.
 82 
 83     - `source` -- *(req)* The drag source object that should be consulted during
 84       the drag operations. This is usually the container view that initiated
 85       the drag.
 86 
 87     - `dragView` -- Optional view that will be used as the source image for the
 88       drag. The drag operation will clone the DOM elements for this view and
 89       parent them under the drag pane, which has the class name `sc-ghost-view`.
 90       The drag view is not moved from its original location during a drag.
 91       If the dragView is not provided, the source is used as dragView.
 92 
 93     - `ghost` -- `YES` | `NO`  If `NO`, the drag view image will show, but the source
 94       `dragView` will not be hidden.  Set to `YES` to make it appear that the
 95       `dragView` itself is being dragged around.
 96 
 97     - `slideBack` -- `YES` | `NO`  If `YES` and the drag operation is cancelled, the
 98       `dragView` will slide back to its source origin.
 99 
100     - `origin` --  If passed, this will be used as the origin point for the
101       ghostView when it slides back.  You normally do not need to pass this
102       unless the ghost view does not appear in the main UI.
103 
104     - `data` -- Optional hash of data types and values.  You can use this to pass
105       a static set of data instead of providing a dataSource.  If you provide
106       a dataSource, it will be used instead.
107 
108     - `dataSource` --  Optional object that will provide the data for the drag to
109       be consumed by the drop target.  If you do not pass this parameter or the
110       data hash, then the source object will be used if it implements the
111       SC.DragDataSourceProtocol protocol.
112 
113     - `anchorView` -- if you pass this optional view, then the drag will only be
114       allowed to happen within this view.  The ghostView will actually be added
115       as a child of this view during the drag.  Normally the anchorView is the
116       window.
117 
118   @extends SC.Object
119 */
120 SC.Drag = SC.Object.extend(
121 /** @scope SC.Drag.prototype */ {
122 
123   /**
124     The source object used to coordinate this drag.
125 
126     @readOnly
127     @type SC.DragSource
128   */
129   source: null,
130 
131   /**
132     The view actually dragged around the screen. This is created automatically
133     from the dragView.
134 
135     @readOnly
136     @type SC.View
137   */
138   ghostView: null,
139 
140   /**
141     If `YES`, then the `ghostView` will acts like a cursor and attach directly
142     to the mouse/touch location.
143 
144     @readOnly
145     @type Boolean
146   */
147   ghostActsLikeCursor: NO,
148 
149   /**
150     The view that was used as the source of the `ghostView`.
151 
152     The drag view is not moved from its original location during a drag.
153     Instead, the DOM content of the view is cloned and managed by the
154     ghostView.  If you want to visually indicate that the view is being
155     moved, you should set ghost to `YES`.
156     If dragView is not provided the source is used instead.
157 
158     @readOnly
159     @type SC.View
160   */
161   dragView: null,
162 
163   /**
164     If `YES`, the `dragView` is automatically hidden while dragging around the
165     ghost.
166 
167     @readOnly
168     @type Boolean
169   */
170   ghost: YES,
171 
172   /**
173     If `NO`, the source will not be copied, clone, no ghost view will get created,
174     and it won't be moved.
175 
176     @type Boolean
177   */
178   sourceIsDraggable: YES,
179 
180   /**
181     If `YES`, then the `ghostView` will slide back to its original location if
182     drag is cancelled.
183 
184     @type Boolean
185   */
186   slideBack: YES,
187 
188   /**
189     The origin to slide back to in the coordinate of the `dragView`'s
190     containerView.
191 
192     @type Point
193   */
194   ghostOffset: { x: 0, y: 0 },
195 
196   /**
197     The current location of the mouse pointer in window coordinates. This is
198     updated as long as the mouse button is pressed or touch is active. Drop targets are
199     encouraged to update this property in their `dragUpdated()` method
200     implementations.
201 
202     The ghostView will be positioned at this location.
203 
204     @type Point
205   */
206   location: {},
207 
208   // ..........................................
209   // DRAG DATA
210   //
211 
212   /**
213     Data types supported by this drag operation.
214 
215     Returns an array of data types supported by the drag source.  This may be
216     generated dynamically depending on the data source.
217 
218     If you are implementing a drag source, you will need to provide these data
219     types so that drop targets can detect if they can accept your drag data.
220 
221     If you are implementing a drop target, you should inspect this property
222     on your `dragEntered()` and `prepareForDragOperation()` methods to determine
223     if you can handle any of the data types offered up by the drag source.
224 
225     @type Array
226   */
227   dataTypes: function () {
228     // first try to use the data source.
229     if (this.dataSource) return this.dataSource.get('dragDataTypes') || [];
230 
231     // if that fails, get the keys from the data hash.
232     var hash = this.data;
233     if (hash) {
234       var ret = [];
235       for (var key in hash) {
236         if (hash.hasOwnProperty(key)) ret.push(key);
237       }
238       return ret;
239     }
240 
241     // if that fails, then check to see if the source object is a dataSource.
242     var source = this.get('source');
243     if (source && source.dragDataTypes) return source.get('dragDataTypes') || [];
244 
245     // no data types found. :(
246     return [];
247   }.property().cacheable(),
248 
249   /**
250     Checks for a named data type in the drag.
251 
252     @param {String} dataType the data type
253     @returns {Boolean} YES if data type is present in dataTypes array.
254   */
255   hasDataType: function (dataType) {
256     return (this.get('dataTypes').indexOf(dataType) >= 0);
257   },
258 
259   /**
260     Retrieve the data for the specified `dataType` from the drag source.
261 
262     Drop targets can use this method during their `performDragOperation()`
263     method to retrieve the actual data provided by the drag data source.  This
264     data may be generated dynamically depending on the data source.
265 
266     @param {Object} dataType data type you want to retrieve.  Should be one of
267       the values returned in the dataTypes property
268     @returns {Object} The generated data.
269   */
270   dataForType: function (dataType) {
271     // first try to use the data Source.
272     if (this.dataSource) {
273       return this.dataSource.dragDataForType(this, dataType);
274 
275     // then try to use the data hash.
276     } else if (this.data) {
277       return this.data[dataType];
278 
279     // if all else fails, check to see if the source object is a data source.
280     } else {
281       var source = this.get('source');
282       if (source && SC.typeOf(source.dragDataForType) === SC.T_FUNCTION) {
283         return source.dragDataForType(this, dataType);
284 
285       // no data source found. :(
286       } else return null;
287     }
288   },
289 
290   /**
291     Optional object used to provide the data for the drag.
292 
293     Drag source can designate a `dataSource` object to generate the data for
294     a drag dynamically.  The data source can and often is the drag source
295     object itself.
296 
297     Data Source objects must comply with the `SC.DragDataSourceProtocol` interface.  If
298     you do not want to implement this interface, you can provide the data
299     directly with the data property.
300 
301     If you are implementing a drop target, use the dataTypes property and
302     `dataForTypes()` method to access data instead of working directly with
303     these properties.
304 
305     @readOnly
306     @type SC.DragDataSourceProtocol
307   */
308   dataSource: null,
309 
310   /**
311     Optional hash of data.  Used if no dataSource was provided.
312 
313     Drag sources can provide a hash of data when the drag begins instead of
314     specifying an actual dataSource.  The data is stored in this property.
315     If you are implementing a drop target, use the dataTypes property and
316     `dataForTypes()` method to access data instead of working directly with
317     these properties.
318 
319     @readOnly
320     @type Hash
321   */
322   data: null,
323 
324   /**
325     Returns the currently allowed `dragOperations` for the drag.  This will be
326     set just before any callbacks are invoked on a drop target.  The drag
327     source is given an opportunity to set these operations.
328 
329     @readOnly
330     @type Number
331   */
332   allowedDragOperations: SC.DRAG_ANY,
333 
334   /** @private required by autoscroll */
335   _dragInProgress: YES,
336 
337   /** @private
338     Stores the initial visibility state of the dragView so it can be restored
339     after the drag
340   */
341   _dragViewWasVisible: null,
342 
343   /** @private
344     This will actually start the drag process. Called by SC.Drag.start().
345   */
346   startDrag: function () {
347     if (this.get('sourceIsDraggable')) {
348       // create the ghost view
349       this._createGhostView();
350     }
351 
352     var evt = this.event;
353 
354     // compute the ghost offset from the original start location
355 
356     var loc = { x: evt.pageX, y: evt.pageY };
357     this.set('location', loc);
358 
359     if (this.get('sourceIsDraggable')) {
360       var dv = this._getDragView();
361       var pv = dv.get('parentView');
362 
363       // convert to global coordinates
364       var origin = pv ? pv.convertFrameToView(dv.get('frame'), null) : dv.get('frame');
365 
366       if (this.get('ghost')) {
367         // Hide the dragView
368         this._dragViewWasVisible = dv.get('isVisible');
369         dv.set('isVisible', NO);
370       }
371 
372       if (this.ghostActsLikeCursor) this.ghostOffset = { x: 14, y: 14 };
373       else this.ghostOffset = { x: (loc.x - origin.x), y: (loc.y - origin.y) };
374 
375       // position the ghost view
376       if (!this._ghostViewHidden) this._positionGhostView(evt);
377 
378       if (evt.makeTouchResponder) {
379         // Should use invokeLater if I can figure it out
380         var self = this;
381         SC.Timer.schedule({
382           target: evt,
383           action: function () {
384             if (!evt.hasEnded) evt.makeTouchResponder(self, YES);
385           },
386           interval: 1
387         });
388       }
389       else {
390         // notify root responder that a drag is in process
391         this.ghostView.rootResponder.dragDidStart(this, evt);
392       }
393     }
394 
395     var source = this.source;
396     if (source && source.dragDidBegin) source.dragDidBegin(this, loc);
397 
398     // let all drop targets know that a drag has started
399     var ary = this._dropTargets();
400 
401     for (var idx = 0, len = ary.length; idx < len; idx++) {
402       var target = ary[idx];
403       // If the target is not visible, it is not valid.
404       if (!target.get('isVisibleInWindow')) continue;
405 
406       target.tryToPerform('dragStarted', this, evt);
407     }
408   },
409 
410   /** @private
411     Cancel the drag operation.
412 
413     This is called by RootResponder's keyup method when the user presses
414     escape and a drag is in progress.
415 
416     @param {Event} evt the key event
417     @see SC.Drag.endDrag
418   */
419   cancelDrag: function (evt) {
420     var target = this._lastTarget;
421 
422     if (target && target.dragExited) target.dragExited(this, this._lastMouseDraggedEvent);
423 
424     this.endDrag(evt, SC.DRAG_NONE);
425   },
426 
427   /** @private
428     End the drag operation.
429 
430     This notifies the data source that the drag ended and removes the
431     ghost view, but does not notify the drop target of a drop.
432 
433     @param {Event} evt
434     @param {DragOp} op The drag operation that was performed. One of
435       SC.DRAG_COPY, SC.DRAG_MOVE, SC.DRAG_LINK, or SC.DRAG_NONE.
436   */
437   endDrag: function (evt, op) {
438     var loc = this.get('location');
439 
440     // notify all drop targets that the drag ended
441     var ary = this._dropTargets();
442     for (var idx = 0, len = ary.length; idx < len; idx++) {
443       try {
444         ary[idx].tryToPerform('dragEnded', this, evt);
445       } catch (ex2) {
446         SC.Logger.error('Exception in SC.Drag.mouseUp(dragEnded on %@): %@'.fmt(ary[idx], ex2));
447       }
448     }
449 
450     // Trigger a slide-back if triggered and if the drag was unsuccessful.
451     if (this.get('sourceIsDraggable') && this.get('slideBack') && op === SC.DRAG_NONE) {
452       this._slideGhostViewBack();
453     }
454     // Otherwise, wrap up the drag right now.
455     else {
456       this._endDrag();
457     }
458 
459     var source = this.get('source');
460     if (source) {
461       // notify the source that the drag succeeded
462       if (source.dragDidSucceed && op !== SC.DRAG_NONE) source.dragDidSucceed(this, loc, op);
463       // notify the source that the drag was cancelled
464       else if (source.dragDidCancel && op === SC.DRAG_NONE) source.dragDidCancel(this, loc, op);
465 
466       // always notify the source that everything has completed
467       if (source.dragDidEnd) source.dragDidEnd(this, loc, op);
468     }
469   },
470 
471   // ..........................................
472   // PRIVATE PROPERTIES AND METHODS
473   //
474 
475   /** @private */
476   touchStart: function (evt) {
477     return YES;
478   },
479 
480   /** @private
481     This method is called repeatedly during a mouse drag.  It updates the
482     position of the ghost image, then it looks for a current drop target and
483     notifies it.
484   */
485   mouseDragged: function (evt) {
486     var scrolled = this._autoscroll(evt);
487     var loc = this.get('location');
488     if (!scrolled && (evt.pageX === loc.x) && (evt.pageY === loc.y)) {
489       return; // quickly ignore duplicate calls
490     }
491 
492     // save the new location to avoid duplicate mouseDragged event processing
493     loc = { x: evt.pageX, y: evt.pageY };
494     this.set('location', loc);
495     this._lastMouseDraggedEvent = evt;
496 
497     // STEP 1: Determine the deepest drop target that allows an operation.
498     // if the drop target selected the last time this method was called
499     // differs from the deepest target found, then go up the chain until we
500     // either hit the last one or find one that will allow a drag operation
501     var source = this.source;
502     var last = this._lastTarget;
503     var target = this._findDropTarget(evt); // deepest drop target
504     var op = SC.DRAG_NONE;
505 
506     while (target && (target !== last) && (op === SC.DRAG_NONE)) {
507       // make sure the drag source will permit a drop operation on the named
508       // target
509       if (target && source && source.dragSourceOperationMaskFor) {
510         op = source.dragSourceOperationMaskFor(this, target);
511       } else op = SC.DRAG_ANY; // assume drops are allowed
512 
513       // now, let's see if the target will accept the drag
514       if ((op !== SC.DRAG_NONE) && target && target.computeDragOperations) {
515         op = op & target.computeDragOperations(this, evt, op);
516       } else op = SC.DRAG_NONE; // assume drops AREN'T allowed
517 
518       this.allowedDragOperations = op;
519 
520       // if DRAG_NONE, then look for the next parent that is a drop zone
521       if (op === SC.DRAG_NONE) target = this._findNextDropTarget(target);
522     }
523 
524     // STEP 2: Refocus the drop target if needed
525     if (target !== last) {
526       if (last && last.dragExited) last.dragExited(this, evt);
527 
528       if (target) {
529         if (target.dragEntered) target.dragEntered(this, evt);
530         if (target.dragUpdated) target.dragUpdated(this, evt);
531       }
532 
533       this._lastTarget = target;
534     } else {
535       if (target && target.dragUpdated) target.dragUpdated(this, evt);
536     }
537 
538     // notify source that the drag moved
539     if (source && source.dragDidMove) source.dragDidMove(this, loc);
540 
541     // reposition the ghostView
542     if (this.get('sourceIsDraggable') && !this._ghostViewHidden) this._positionGhostView(evt);
543   },
544 
545   touchesDragged: function (evt) {
546     this.mouseDragged(evt);
547   },
548 
549   /**
550     @private
551 
552     Called when the mouse is released.  Performs any necessary cleanup and
553     executes the drop target protocol to try to complete the drag operation.
554   */
555   mouseUp: function (evt) {
556     var loc    = { x: evt.pageX, y: evt.pageY },
557         target = this._lastTarget,
558         op     = this.allowedDragOperations;
559 
560     this.set('location', loc);
561 
562     // try to have the drop target perform the drop...
563     try {
564       if (target && target.acceptDragOperation && target.acceptDragOperation(this, op)) {
565         op = target.performDragOperation ? target.performDragOperation(this, op) : SC.DRAG_NONE;
566       } else {
567         op = SC.DRAG_NONE;
568       }
569     } catch (e) {
570       SC.Logger.error('Exception in SC.Drag.mouseUp(acceptDragOperation|performDragOperation): %@'.fmt(e));
571     }
572 
573     this.endDrag(evt, op);
574   },
575 
576   /** @private */
577   touchEnd: function (evt) {
578     this.mouseUp(evt);
579   },
580 
581   /** @private
582     Returns the dragView. If it is not set, the source is returned.
583   */
584   _getDragView: function () {
585     if (!this.dragView) {
586       if (!this.source || !this.source.isView) throw new Error("Source can't be used as dragView, because it's not a view.");
587       this.dragView = this.source;
588     }
589     return this.dragView;
590   },
591 
592   /** @private
593     This will create the ghostView and add it to the document.
594   */
595   _createGhostView: function () {
596     var dragView = this._getDragView(),
597         frame = dragView.get('borderFrame'),
598         ghostLayout, view;
599 
600     // Create a fixed layout for the ghost view.
601     ghostLayout = { top: frame.y, left: frame.x, width: frame.width, height: frame.height };
602 
603     view = this.ghostView = SC.Pane.create({
604       classNames: ['sc-ghost-view'],
605       layout: ghostLayout,
606       owner: this,
607       wantsAcceleratedLayer: dragView.get('wantsAcceleratedLayer'),
608 
609       didCreateLayer: function () {
610         if (dragView) {
611           var dragLayer = dragView.get('layer');
612           if (dragLayer) {
613             var layer = dragLayer.cloneNode(true);
614 
615             // Canvas elements need manual copying.
616             var dragCanvasses = dragView.$().find('canvas');
617             if (dragCanvasses.length) {
618               var ghostCanvasses = $(layer).find('canvas'),
619                   len = dragCanvasses.length,
620                   i, dragCanvas, ghostCanvas;
621               for (i = 0; i < len; i++) {
622                 dragCanvas = dragCanvasses[i];
623                 ghostCanvas = ghostCanvasses[i];
624                 ghostCanvas.width = dragCanvas.width;
625                 ghostCanvas.height = dragCanvas.height;
626                 ghostCanvas.getContext('2d').drawImage(dragCanvas, 0, 0);
627               }
628             }
629 
630             // Make sure the layer we put in the ghostView wrapper is not displaced.
631             layer.style.top = "0px";
632             layer.style.left = "0px";
633             layer.style.bottom = "0px";
634             layer.style.right = "0px";
635 
636             // Attach the cloned layer.
637             this.get('layer').appendChild(layer);
638           }
639         }
640       }
641     });
642 
643     view.append();  // add to window
644   },
645 
646   /** @private
647     Positions the ghost view underneath the mouse/touch with the initial offset
648     recorded by when the drag started.
649   */
650   _positionGhostView: function (evt) {
651     var ghostView = this.ghostView,
652       loc;
653 
654     if (ghostView) {
655       loc = this.get('location');
656 
657       loc.x -= this.ghostOffset.x;
658       loc.y -= this.ghostOffset.y;
659       ghostView.adjust({ top: loc.y, left: loc.x });
660     }
661   },
662 
663   /** @private
664     YES if the ghostView has been manually hidden.
665 
666     @type Boolean
667     @default NO
668   */
669   _ghostViewHidden: NO,
670 
671   /**
672     Hide the ghostView.
673   */
674   hideGhostView: function () {
675     if (this.ghostView && !this._ghostViewHidden) {
676       this.ghostView.remove();
677       this._ghostViewHidden = YES;
678     }
679   },
680 
681   /**
682     Unhide the ghostView.
683   */
684   unhideGhostView: function () {
685     if (this._ghostViewHidden) {
686       this._ghostViewHidden = NO;
687       this._createGhostView();
688     }
689   },
690 
691   /** @private Called instead of _destroyGhostView if slideBack is YES. */
692   _slideGhostViewBack: function () {
693     if (this.ghostView) {
694       var dragView = this._getDragView(),
695           frame = dragView.get('borderFrame'),
696           dragParentView = dragView.get('parentView'),
697           globalOrigin = dragParentView ? dragParentView.convertFrameToView(frame, null) : dragView.get('frame'),
698           slidebackLayout;
699 
700       // Create a fixed layout for the ghost view.
701       slidebackLayout = { top: globalOrigin.y, left: globalOrigin.x };
702 
703       // Animate the ghost view back to its original position; destroy after.
704       this.ghostView.animate(slidebackLayout, 0.5, this, function () {
705         this.invokeNext(function() {
706           // notify the source that slideback has completed
707           var source = this.get('source');
708           if (this.get('slideBack') && source && source.dragSlideBackDidEnd) source.dragSlideBackDidEnd(this);
709 
710           this._endDrag();
711         });
712       });
713 
714     }
715     else {
716       this._endDrag();
717     }
718   },
719 
720   /** @private */
721   _destroyGhostView: function () {
722     if (this.ghostView) {
723       this.ghostView.remove();
724       this.ghostView = null; // this will allow the GC to collect it.
725       this._ghostViewHidden = NO;
726     }
727   },
728 
729   /** @private */
730   _endDrag: function () {
731     if (this.get('sourceIsDraggable')) {
732       this._destroyGhostView();
733       if (this.get('ghost')) {
734         // Show the dragView if it was hidden.
735         if (this._dragViewWasVisible) this._getDragView().set('isVisible', YES);
736         this._dragViewWasVisible = null;
737       }
738     }
739 
740     this._cleanUpDrag();
741   },
742 
743   /** @private */
744   _cleanUpDrag: function () {
745     this._lastTarget = null;
746     this._dragInProgress = NO;
747     this._cachedDropTargets = null;
748   },
749 
750   /** @private
751     Return an array of drop targets, sorted with any nested drop targets
752     at the top of the array.  The first time this method is called during
753     a drag, it will reconstruct this array using the current set of
754     drop targets.  Afterwards it uses the cached set until the drop
755     completes.
756 
757     This means that if you change the view hierarchy of your drop targets
758     during a drag, it will probably be wrong.
759   */
760   _dropTargets: function () {
761     if (this._cachedDropTargets) return this._cachedDropTargets;
762 
763     // build array of drop targets
764     var ret = [];
765     var hash = SC.Drag._dropTargets;
766     for (var key in hash) {
767       if (hash.hasOwnProperty(key)) ret.push(hash[key]);
768     }
769 
770     // views must be sorted so that drop targets with the deepest nesting
771     // levels appear first in the array.  The getDepthFor().
772     var depth = {};
773     var dropTargets = SC.Drag._dropTargets;
774     var getDepthFor = function (x) {
775       if (!x) return 0;
776       var guid = SC.guidFor(x);
777       var ret = depth[guid];
778       if (!ret) {
779         ret = 1;
780         while ((x = x.get('parentView'))) {
781           if (dropTargets[SC.guidFor(x)] !== undefined) ret = ret + 1;
782           if (x.isPane && x.isMainPane) ret = ret + 10000; // Arbitrary value always have the main pain on top
783         }
784         depth[guid] = ret;
785       }
786       return ret;
787     };
788 
789     // sort array of drop targets
790     ret.sort(function (a, b) {
791       if (a === b) return 0;
792       a = getDepthFor(a);
793       b = getDepthFor(b);
794       return (a > b) ? -1 : 1;
795     });
796 
797     this._cachedDropTargets = ret;
798     return ret;
799   },
800 
801   /** @private
802     This will search through the drop targets, looking for one in the target
803     area.
804   */
805   _findDropTarget: function (evt) {
806     var loc = { x: evt.pageX, y: evt.pageY };
807 
808     var target, frame;
809     var ary = this._dropTargets();
810     for (var idx = 0, len = ary.length; idx < len; idx++) {
811       target = ary[idx];
812 
813       // If the target is not visible, it is not valid.
814       if (!target.get('isVisibleInWindow')) continue;
815 
816       // get clippingFrame, converted to the pane.
817       frame = target.convertFrameToView(target.get('clippingFrame'), null);
818 
819       // check to see if loc is inside.  If so, then make this the drop target
820       // unless there is a drop target and the current one is not deeper.
821       if (SC.pointInRect(loc, frame)) return target;
822     }
823     return null;
824   },
825 
826   /** @private
827     Search the parent nodes of the target to find another view matching the
828     drop target.  Returns null if no matching target is found.
829   */
830   _findNextDropTarget: function (target) {
831     var dropTargets = SC.Drag._dropTargets;
832     while ((target = target.get('parentView'))) {
833       if (dropTargets[SC.guidFor(target)]) return target;
834     }
835     return null;
836   },
837 
838   // ............................................
839   // AUTOSCROLLING
840   //
841 
842   /** @private
843     Performs auto-scrolling for the drag.  This will only do anything if
844     the user keeps the mouse/touch within a few pixels of one location for a little
845     while.
846 
847     Returns YES if a scroll was performed.
848   */
849   _autoscroll: function (evt) {
850     if (!evt) evt = this._lastAutoscrollEvent;
851 
852     // If drag has ended, exit
853     if (!this._dragInProgress) return NO;
854 
855     // STEP 1: Find the first view that we can actually scroll.  This view
856     // must be:
857     // - scrollable
858     // - the mouse pointer or touch must be within a scrolling hot zone
859     // - there must be room left to scroll in that direction.
860 
861     // NOTE: an event is passed only when called from mouseDragged
862     var loc  = evt ? { x: evt.pageX, y: evt.pageY } : this.get('location'),
863         view = this._findScrollableView(loc),
864         scrollableView = null, // become final view when found
865         vscroll, hscroll, min, max, container, f;
866 
867     // hscroll and vscroll will become either 1 or -1 to indicate scroll
868     // direction or 0 for no scroll.
869 
870     while (view && !scrollableView) {
871 
872       // quick check...can we scroll this view right now?
873       vscroll = view.get('canScrollVertical') ? 1 : 0;
874       hscroll = view.get('canScrollHorizontal') ? 1 : 0;
875 
876       // at least one direction might be scrollable.  Collect frame info
877       if (vscroll || hscroll) {
878         container = view.get('containerView');
879         if (container) {
880           f = view.convertFrameToView(container.get('frame'), null);
881         } else {
882           vscroll = hscroll = 0; // can't autoscroll this mother
883         }
884       }
885 
886       // handle vertical direction
887       if (vscroll) {
888 
889         // bottom hotzone?
890         max = SC.maxY(f);
891         min = max - SC.DRAG_AUTOSCROLL_ZONE_THICKNESS;
892         if (loc.y >= min && loc.y <= max) vscroll = 1;
893         else {
894           // how about top
895           min = SC.minY(f);
896           max = min + SC.DRAG_AUTOSCROLL_ZONE_THICKNESS;
897           if (loc.y >= min && loc.y <= max) vscroll = -1;
898           else vscroll = 0; // can't scroll vertical
899         }
900       }
901 
902       // handle horizontal direction
903       if (hscroll) {
904 
905         // bottom hotzone?
906         max = SC.maxX(f);
907         min = max - SC.DRAG_AUTOSCROLL_ZONE_THICKNESS;
908         if (loc.x >= min && loc.x <= max) hscroll = 1;
909         else {
910           // how about top
911           min = SC.minX(f);
912           max = min + SC.DRAG_AUTOSCROLL_ZONE_THICKNESS;
913           if (loc.x >= min && loc.x <= max) hscroll = -1;
914           else hscroll = 0; // can't scroll vertical
915         }
916       }
917 
918       // if we can scroll, then set this.
919       if (vscroll || hscroll) scrollableView = view;
920       else view = this._findNextScrollableView(view);
921     }
922 
923     // STEP 2: Only scroll if the user remains within the hot-zone for a
924     // period of time
925     if (scrollableView && (this._lastScrollableView === scrollableView)) {
926       if ((Date.now() - this._hotzoneStartTime) > 100) {
927         this._horizontalScrollAmount *= 1.05;
928         this._verticalScrollAmount *= 1.05; // accelerate scroll
929       }
930 
931     // otherwise, reset everything and disallow scroll
932     } else {
933       this._lastScrollableView = scrollableView;
934       this._horizontalScrollAmount = 15;
935       this._verticalScrollAmount = 15;
936       this._hotzoneStartTime = (scrollableView) ? Date.now() : null;
937       hscroll = vscroll = 0;
938     }
939 
940     // STEP 3: Scroll!
941     if (scrollableView && (hscroll || vscroll)) {
942       var scroll = {
943         x: hscroll * this._horizontalScrollAmount,
944         y: vscroll * this._verticalScrollAmount
945       };
946       scrollableView.scrollBy(scroll);
947     }
948 
949     // If a scrollable view was found, then check later
950     if (scrollableView) {
951       if (evt) {
952         this._lastAutoscrollEvent = { pageX: evt.pageX, pageY: evt.pageY };
953       }
954       this.invokeLater(this._autoscroll, 100, null);
955       return YES;
956     } else {
957       this._lastAutoscrollEvent = null;
958       return NO;
959     }
960   },
961 
962   /** @private
963     Returns an array of scrollable views, sorted with nested scrollable views
964     at the top of the array.  The first time this method is called during a
965     drag, it will reconstruct this array using the current state of scrollable
966     views.  Afterwards it uses the cached set until the drop completes.
967   */
968   _scrollableViews: function () {
969     if (this._cachedScrollableView) return this._cachedScrollableView;
970 
971     // build array of scrollable views
972     var ret = [];
973     var hash = SC.Drag._scrollableViews;
974     for (var key in hash) {
975       if (hash.hasOwnProperty(key)) ret.push(hash[key]);
976     }
977 
978     // now resort.  This custom function will sort nested scrollable views
979     // at the start of the list.
980     ret = ret.sort(function (a, b) {
981       var view = a;
982       while ((view = view.get('parentView'))) {
983         if (b === view) return -1;
984       }
985       return 1;
986     });
987 
988     this._cachedScrollableView = ret;
989     return ret;
990   },
991 
992   /** @private
993     This will search through the scrollable views, looking for one in the
994     target area.
995   */
996   _findScrollableView: function (loc) {
997     var ary = this._scrollableViews(),
998         len = ary ? ary.length : 0,
999         target, frame, idx;
1000 
1001     for (idx = 0; idx < len; idx++) {
1002       target = ary[idx];
1003 
1004       if (!target.get('isVisibleInWindow')) continue;
1005 
1006       // get clippingFrame, converted to the pane
1007       frame = target.convertFrameToView(target.get('clippingFrame'), null);
1008 
1009       // check to see if loc is inside
1010       if (SC.pointInRect(loc, frame)) return target;
1011     }
1012     return null;
1013   },
1014 
1015   /** @private
1016     Search the parent nodes of the target to find another scrollable view.
1017     return null if none is found.
1018   */
1019   _findNextScrollableView: function (view) {
1020     var scrollableViews = SC.Drag._scrollableViews;
1021     while ((view = view.get('parentView'))) {
1022       if (scrollableViews[SC.guidFor(view)]) return view;
1023     }
1024     return null;
1025   }
1026 
1027 });
1028 
1029 SC.Drag.mixin(
1030 /** @scope SC.Drag */ {
1031 
1032   /**
1033    This is the method you use to initiate a new drag.  See class documentation
1034    for more info on the options taken by this method.
1035 
1036    @params {Hash} ops a hash of options.  See documentation above.
1037   */
1038   start: function (ops) {
1039     var ret = this.create(ops);
1040     ret.startDrag();
1041     return ret;
1042   },
1043 
1044   /** @private */
1045   _dropTargets: {},
1046 
1047   /** @private */
1048   _scrollableViews: {},
1049 
1050   /**
1051     Register the view object as a drop target.
1052 
1053     This method is called automatically whenever a view is created with the
1054     isDropTarget property set to `YES`.  You generally will not need to call it
1055     yourself.
1056 
1057     @param {SC.View} target a view implementing the SC.DropTargetProtocol protocol
1058   */
1059   addDropTarget: function (target) {
1060     this._dropTargets[SC.guidFor(target)] = target;
1061   },
1062 
1063   /**
1064     Unregister the view object as a drop target.
1065 
1066     This method is called automatically whenever a view is removed from the
1067     hierarchy.  You generally will not need to call it yourself.
1068 
1069     @param {SC.View} target A previously registered drop target
1070   */
1071   removeDropTarget: function (target) {
1072     delete this._dropTargets[SC.guidFor(target)];
1073   },
1074 
1075   /**
1076     Register the view object as a scrollable view.  These views will
1077     auto-scroll during a drag.
1078 
1079     @param {SC.View} target The view that should be auto-scrolled
1080   */
1081   addScrollableView: function (target) {
1082     this._scrollableViews[SC.guidFor(target)] = target;
1083   },
1084 
1085   /**
1086     Remove the view object as a scrollable view.  These views will auto-scroll
1087     during a drag.
1088 
1089     @param {SC.View} target A previously registered scrollable view
1090   */
1091   removeScrollableView: function (target) {
1092     delete this._scrollableViews[SC.guidFor(target)];
1093   }
1094 
1095 });
1096