// ======================================================================== // SproutCore -- JavaScript Application Framework // Copyright ©2006-2011, Strobe Inc. and contributors. // Portions copyright ©2008 Apple Inc. All rights reserved. // ======================================================================== SC.DRAG_LINK = 0x0004; SC.DRAG_COPY = 0x0001; SC.DRAG_MOVE = 0x0002; SC.DRAG_NONE = 0x0000; SC.DRAG_ANY = 0x000F; SC.DRAG_DATA = 0x0008; // includes SC.DRAG_REORDER SC.DRAG_AUTOSCROLL_ZONE_THICKNESS = 20; // Add draggable and scrollable support to SC.View SC.View.reopen( /** @scope SC.View.prototype */ { init: function(original) { original(); // register for drags if (this.get('isDropTarget')) { SC.Drag.addDropTarget(this) ; } // register scroll views for autoscroll during drags if (this.get('isScrollable')) { SC.Drag.addScrollableView(this) ; } }.enhance(), destroy: function(original) { // unregister for drags if (this.get('isDropTarget')) { SC.Drag.removeDropTarget(this) ; } // unregister for autoscroll during drags if (this.get('isScrollable')) { SC.Drag.removeScrollableView(this) ; } return original(); }.enhance() }); /** @class An instance of this object is created whenever a drag occurs. The instance manages the mouse/touch events and coordinating with droppable targets until the user releases the mouse button. To initiate a drag, you should call SC.Drag.start() with the options below specified in a hash. Pass the ones you need to get the drag you want: - *event: (req)* The mouse event/touch that triggered the drag. This will be used to position the element. - *source: (req)* The drag source object that should be consulted during the drag operations. This is usually the container view that initiated the drag. - *dragView: * Optional view that will be used as the source image for the drag. The drag operation will clone the DOM elements for this view and parent them under the drag pane, which has the class name 'sc-ghost-view'. The drag view is not moved from its original location during a drag. If the dragView is not provided, the source is used as dragView. - *ghost: YES | NO* If NO, the drag view image will show, but the source dragView will not be hidden. Set to YES to make it appear that the dragView itself is being dragged around. - *slideBack: YES | NO* If YES and the drag operation is cancelled, the dragView will slide back to its source origin. - *origin:* If passed, this will be used as the origin point for the ghostView when it slides back. You normally do not need to pass this unless the ghost view does not appear in the main UI. - *data:* Optional hash of data types and values. You can use this to pass a static set of data instead of providing a dataSource. If you provide a dataSource, it will be used instead. - *dataSource:* Optional object that will provide the data for the drag to be consumed by the drop target. If you do not pass this parameter or the data hash, then the source object will be used if it implements the SC.DragDataSource protocol. - *anchorView:* if you pass this optional view, then the drag will only be allowed to happen within this view. The ghostView will actually be added as a child of this view during the drag. Normally the anchorView is the window. @extends SC.Object */ SC.Drag = SC.Object.extend( /** @scope SC.Drag.prototype */ { /** The source object used to coordinate this drag. @readOnly @type SC.DragSource */ source: null, /** The view actually dragged around the screen. This is created automatically from the dragView. @readOnly @type SC.View */ ghostView: null, /** If YES, then the ghostView will acts like a cursor and attach directly to the mouse/touch location. @readOnly @type Boolean */ ghostActsLikeCursor: NO, /** The view that was used as the source of the ghostView. The drag view is not moved from its original location during a drag. Instead, the DOM content of the view is cloned and managed by the ghostView. If you want to visually indicate that the view is being moved, you should set ghost to YES. If dragView is not provided the source is used instead. @readOnly @type SC.View */ dragView: null, /** If YES, the dragView is automatically hidden while dragging around the ghost. @readOnly @type Boolean */ ghost: YES, /** If NO, the source will not be copied, clone, no ghost view will get created, and it won't be moved. @type Boolean */ sourceIsDraggable: YES, /** If YES, then the ghostView will slide back to its original location if drag is cancelled. @type Boolean */ slideBack: YES, /** The origin to slide back to in the coordinate of the dragView's containerView. @type Point */ ghostOffset: { x: 0, y: 0 }, /** The current location of the mouse pointer in window coordinates. This is updated as long as the mouse button is pressed or touch is active. Drop targets are encouraged to update this property in their dragUpdated() method implementations. The ghostView will be positioned at this location. @type Point */ location: {}, // .......................................... // DRAG DATA // /** Data types supported by this drag operation. Returns an array of data types supported by the drag source. This may be generated dynamically depending on the data source. If you are implementing a drag source, you will need to provide these data types so that drop targets can detect if they can accept your drag data. If you are implementing a drop target, you should inspect this property on your dragEntered() and prepareForDragOperation() methods to determine if you can handle any of the data types offered up by the drag source. @property {Array} available data types */ dataTypes: function() { // first try to use the data source. if (this.dataSource) return this.dataSource.get('dragDataTypes') || [] ; // if that fails, get the keys from the data hash. var hash = this.data ; if (hash) { var ret = []; for (var key in hash) { if (hash.hasOwnProperty(key)) ret.push(key) ; } return ret ; } // if that fails, then check to see if the source object is a dataSource. var source = this.get('source') ; if (source && source.dragDataTypes) return source.get('dragDataTypes') || [] ; // no data types found. :( return [] ; }.property().cacheable(), /** Checks for a named data type in the drag. @param dataType {String} the data type @returns {Boolean} YES if data type is present in dataTypes array. */ hasDataType: function(dataType) { return (this.get('dataTypes').indexOf(dataType) >= 0) ; }, /** Retrieve the data for the specified dataType from the drag source. Drop targets can use this method during their performDragOperation() method to retrieve the actual data provided by the drag data source. This data may be generated dynamically depending on the data source. @param {Object} dataType data type you want to retrieve. Should be one of the values returned in the dataTypes property @returns {Object} The generated data. */ dataForType: function(dataType) { // first try to use the data Source. if (this.dataSource) { return this.dataSource.dragDataForType(this, dataType) ; // then try to use the data hash. } else if (this.data) { return this.data[dataType]; // if all else fails, check to see if the source object is a data source. } else { var source = this.get('source') ; if (source && SC.typeOf(source.dragDataForType) == SC.T_FUNCTION) { return source.dragDataForType(this, dataType) ; // no data source found. :( } else return null ; } }, /** Optional object used to provide the data for the drag. Drag source can designate a dataSource object to generate the data for a drag dynamically. The data source can and often is the drag source object itself. Data Source objects must comply with the SC.DragDataSource interface. If you do not want to implement this interface, you can provide the data directly with the data property. If you are implementing a drop target, use the dataTypes property and dataForTypes() method to access data instead of working directly with these properties. @readOnly @type SC.DragDataSource */ dataSource: null, /** Optional hash of data. Used if no dataSource was provided. Drag sources can provide a hash of data when the drag begins instead of specifying an actual dataSource. The data is stored in this property. If you are implementing a drop target, use the dataTypes property and dataForTypes() method to access data instead of working directly with these properties. @readOnly @type Hash */ data: null, /** Returns the currently allowed dragOperations for the drag. This will be set just before any callbacks are invoked on a drop target. The drag source is given an opportunity to set these operations. @readOnly @type Number */ allowedDragOperations: SC.DRAG_ANY, /** @private required by autoscroll */ _dragInProgress: YES, /** @private Stores the initial visibililty state of the dragView so it can be restored after the drag */ _dragViewWasVisible: null, /** @private This will actually start the drag process. Called by SC.Drag.start(). */ startDrag: function() { if (this.get('sourceIsDraggable')) { // create the ghost view this._createGhostView() ; } var evt = this.event ; // compute the ghost offset from the original start location var loc = { x: evt.pageX, y: evt.pageY } ; this.set('location', loc) ; if (this.get('sourceIsDraggable')) { var dv = this._getDragView() ; var pv = dv.get('parentView') ; // convert to global cooridinates var origin = pv ? pv.convertFrameToView(dv.get('frame'), null) : dv.get('frame') ; if (this.get('ghost')) { // Hide the dragView this._dragViewWasVisible = dv.get('isVisible') ; dv.set('isVisible', NO) ; } if (this.ghostActsLikeCursor) this.ghostOffset = { x: 14, y: 14 }; else this.ghostOffset = { x: (loc.x-origin.x), y: (loc.y-origin.y) } ; // position the ghost view if(!this._ghostViewHidden) this._positionGhostView(evt) ; if (evt.makeTouchResponder) { // Should use invokeLater if I can figure it out var self = this; SC.Timer.schedule({ target: evt, action: function(){ if (!evt.hasEnded) evt.makeTouchResponder(self, YES); }, interval: 1 }); } else { // notify root responder that a drag is in process this.ghostView.rootResponder.dragDidStart(this, evt) ; } } var source = this.source ; if (source && source.dragDidBegin) source.dragDidBegin(this, loc) ; // let all drop targets know that a drag has started var ary = this._dropTargets() ; for (var idx=0, len=ary.length; idx b) ? -1 : 1 ; }) ; this._cachedDropTargets = ret ; return ret ; }, /** @private This will search through the drop targets, looking for one in the target area. */ _findDropTarget: function(evt) { var loc = { x: evt.pageX, y: evt.pageY } ; var target, frame ; var ary = this._dropTargets() ; for (var idx=0, len=ary.length; idx= min && loc.y <= max) vscroll = 1 ; else { // how about top min = SC.minY(f); max = min + SC.DRAG_AUTOSCROLL_ZONE_THICKNESS ; if (loc.y >= min && loc.y <= max) vscroll = -1 ; else vscroll = 0 ; // can't scroll vertical } } // handle horizontal direction if (hscroll) { // bottom hotzone? max = SC.maxX(f); min = max - SC.DRAG_AUTOSCROLL_ZONE_THICKNESS ; if (loc.x >= min && loc.x <= max) hscroll = 1 ; else { // how about top min = SC.minX(f); max = min + SC.DRAG_AUTOSCROLL_ZONE_THICKNESS ; if (loc.x >= min && loc.x <= max) hscroll = -1 ; else hscroll = 0 ; // can't scroll vertical } } // if we can scroll, then set this. if (vscroll || hscroll) scrollableView = view ; else view = this._findNextScrollableView(view) ; } // STEP 2: Only scroll if the user remains within the hot-zone for a // period of time if (scrollableView && (this._lastScrollableView === scrollableView)) { if ((Date.now() - this._hotzoneStartTime) > 100) { this._horizontalScrollAmount *= 1.05 ; this._verticalScrollAmount *= 1.05 ; // accelerate scroll } // otherwise, reset everything and disallow scroll } else { this._lastScrollableView = scrollableView ; this._horizontalScrollAmount = 15 ; this._verticalScrollAmount = 15 ; this._hotzoneStartTime = (scrollableView) ? Date.now() : null ; hscroll = vscroll = 0 ; } // STEP 3: Scroll! if (scrollableView && (hscroll || vscroll)) { var scroll = { x: hscroll * this._horizontalScrollAmount, y: vscroll * this._verticalScrollAmount } ; scrollableView.scrollBy(scroll) ; } // If a scrollable view was found, then check later if (scrollableView) { if (evt) { this._lastAutoscrollEvent = { pageX: evt.pageX, pageY: evt.pageY }; } this.invokeLater(this._autoscroll, 100, null); return YES ; } else { this._lastAutoscrollEvent = null; return NO ; } }, /** @private Returns an array of scrollable views, sorted with nested scrollable views at the top of the array. The first time this method is called during a drag, it will reconstrut this array using the current state of scrollable views. Afterwards it uses the cached set until the drop completes. */ _scrollableViews: function() { if (this._cachedScrollableView) return this._cachedScrollableView ; // build array of scrollable views var ret = [] ; var hash = SC.Drag._scrollableViews ; for (var key in hash) { if (hash.hasOwnProperty(key)) ret.push(hash[key]) ; } // now resort. This custom function will sort nested scrollable views // at the start of the list. ret = ret.sort(function(a,b) { var view = a; while (view = view.get('parentView')) { if (b == view) return -1 ; } return 1; }) ; this._cachedScrollableView = ret ; return ret ; }, /** @private This will search through the scrollable views, looking for one in the target area. */ _findScrollableView: function(loc) { var ary = this._scrollableViews(), len = ary ? ary.length : 0, target, frame, idx; for (idx=0; idx