Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / view / DropZone.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.view.DropZone
17  * @extends Ext.dd.DropZone
18  * @private
19  */
20 Ext.define('Ext.view.DropZone', {
21     extend: 'Ext.dd.DropZone',
22
23     indicatorHtml: '<div class="x-grid-drop-indicator-left"></div><div class="x-grid-drop-indicator-right"></div>',
24     indicatorCls: 'x-grid-drop-indicator',
25
26     constructor: function(config) {
27         var me = this;
28         Ext.apply(me, config);
29
30         // Create a ddGroup unless one has been configured.
31         // User configuration of ddGroups allows users to specify which
32         // DD instances can interact with each other. Using one
33         // based on the id of the View would isolate it and mean it can only
34         // interact with a DragZone on the same View also using a generated ID.
35         if (!me.ddGroup) {
36             me.ddGroup = 'view-dd-zone-' + me.view.id;
37         }
38
39         // The DropZone's encapsulating element is the View's main element. It must be this because drop gestures
40         // may require scrolling on hover near a scrolling boundary. In Ext 4.x two DD instances may not use the
41         // same element, so a DragZone on this same View must use the View's parent element as its element.
42         me.callParent([me.view.el]);
43     },
44
45 //  Fire an event through the client DataView. Lock this DropZone during the event processing so that
46 //  its data does not become corrupted by processing mouse events.
47     fireViewEvent: function() {
48         var me = this,
49             result;
50
51         me.lock();
52         result = me.view.fireEvent.apply(me.view, arguments);
53         me.unlock();
54         return result;
55     },
56
57     getTargetFromEvent : function(e) {
58         var node = e.getTarget(this.view.getItemSelector()),
59             mouseY, nodeList, testNode, i, len, box;
60
61 //      Not over a row node: The content may be narrower than the View's encapsulating element, so return the closest.
62 //      If we fall through because the mouse is below the nodes (or there are no nodes), we'll get an onContainerOver call.
63         if (!node) {
64             mouseY = e.getPageY();
65             for (i = 0, nodeList = this.view.getNodes(), len = nodeList.length; i < len; i++) {
66                 testNode = nodeList[i];
67                 box = Ext.fly(testNode).getBox();
68                 if (mouseY <= box.bottom) {
69                     return testNode;
70                 }
71             }
72         }
73         return node;
74     },
75
76     getIndicator: function() {
77         var me = this;
78
79         if (!me.indicator) {
80             me.indicator = Ext.createWidget('component', {
81                 html: me.indicatorHtml,
82                 cls: me.indicatorCls,
83                 ownerCt: me.view,
84                 floating: true,
85                 shadow: false
86             });
87         }
88         return me.indicator;
89     },
90
91     getPosition: function(e, node) {
92         var y      = e.getXY()[1],
93             region = Ext.fly(node).getRegion(),
94             pos;
95
96         if ((region.bottom - y) >= (region.bottom - region.top) / 2) {
97             pos = "before";
98         } else {
99             pos = "after";
100         }
101         return pos;
102     },
103
104     /**
105      * @private Determines whether the record at the specified offset from the passed record
106      * is in the drag payload.
107      * @param records
108      * @param record
109      * @param offset
110      * @returns {Boolean} True if the targeted record is in the drag payload
111      */
112     containsRecordAtOffset: function(records, record, offset) {
113         if (!record) {
114             return false;
115         }
116         var view = this.view,
117             recordIndex = view.indexOf(record),
118             nodeBefore = view.getNode(recordIndex + offset),
119             recordBefore = nodeBefore ? view.getRecord(nodeBefore) : null;
120
121         return recordBefore && Ext.Array.contains(records, recordBefore);
122     },
123
124     positionIndicator: function(node, data, e) {
125         var me = this,
126             view = me.view,
127             pos = me.getPosition(e, node),
128             overRecord = view.getRecord(node),
129             draggingRecords = data.records,
130             indicator, indicatorY;
131
132         if (!Ext.Array.contains(draggingRecords, overRecord) && (
133             pos == 'before' && !me.containsRecordAtOffset(draggingRecords, overRecord, -1) ||
134             pos == 'after' && !me.containsRecordAtOffset(draggingRecords, overRecord, 1)
135         )) {
136             me.valid = true;
137
138             if (me.overRecord != overRecord || me.currentPosition != pos) {
139
140                 indicatorY = Ext.fly(node).getY() - view.el.getY() - 1;
141                 if (pos == 'after') {
142                     indicatorY += Ext.fly(node).getHeight();
143                 }
144                 me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, indicatorY);
145
146                 // Cache the overRecord and the 'before' or 'after' indicator.
147                 me.overRecord = overRecord;
148                 me.currentPosition = pos;
149             }
150         } else {
151             me.invalidateDrop();
152         }
153     },
154
155     invalidateDrop: function() {
156         if (this.valid) {
157             this.valid = false;
158             this.getIndicator().hide();
159         }
160     },
161
162     // The mouse is over a View node
163     onNodeOver: function(node, dragZone, e, data) {
164         var me = this;
165
166         if (!Ext.Array.contains(data.records, me.view.getRecord(node))) {
167             me.positionIndicator(node, data, e);
168         }
169         return me.valid ? me.dropAllowed : me.dropNotAllowed;
170     },
171
172     // Moved out of the DropZone without dropping.
173     // Remove drop position indicator
174     notifyOut: function(node, dragZone, e, data) {
175         var me = this;
176
177         me.callParent(arguments);
178         delete me.overRecord;
179         delete me.currentPosition;
180         if (me.indicator) {
181             me.indicator.hide();
182         }
183     },
184
185     // The mouse is past the end of all nodes (or there are no nodes)
186     onContainerOver : function(dd, e, data) {
187         var me = this,
188             view = me.view,
189             count = view.store.getCount();
190
191         // There are records, so position after the last one
192         if (count) {
193             me.positionIndicator(view.getNode(count - 1), data, e);
194         }
195
196         // No records, position the indicator at the top
197         else {
198             delete me.overRecord;
199             delete me.currentPosition;
200             me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, 0);
201             me.valid = true;
202         }
203         return me.dropAllowed;
204     },
205
206     onContainerDrop : function(dd, e, data) {
207         return this.onNodeDrop(dd, null, e, data);
208     },
209
210     onNodeDrop: function(node, dragZone, e, data) {
211         var me = this,
212             dropped = false,
213
214             // Create a closure to perform the operation which the event handler may use.
215             // Users may now return <code>false</code> from the beforedrop handler, and perform any kind
216             // of asynchronous processing such as an Ext.Msg.confirm, or an Ajax request,
217             // and complete the drop gesture at some point in the future by calling this function.
218             processDrop = function () {
219                 me.invalidateDrop();
220                 me.handleNodeDrop(data, me.overRecord, me.currentPosition);
221                 dropped = true;
222                 me.fireViewEvent('drop', node, data, me.overRecord, me.currentPosition);
223             },
224             performOperation = false;
225
226         if (me.valid) {
227             performOperation = me.fireViewEvent('beforedrop', node, data, me.overRecord, me.currentPosition, processDrop);
228             if (performOperation !== false) {
229                 // If the processDrop function was called in the event handler, do not do it again.
230                 if (!dropped) {
231                     processDrop();
232                 }
233             }
234         }
235         return performOperation;
236     },
237     
238     destroy: function(){
239         Ext.destroy(this.indicator);
240         delete this.indicator;
241         this.callParent();
242     }
243 });
244