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