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