Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / grid / header / 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.grid.header.DropZone
17  * @extends Ext.dd.DropZone
18  * @private
19  */
20 Ext.define('Ext.grid.header.DropZone', {
21     extend: 'Ext.dd.DropZone',
22     colHeaderCls: Ext.baseCSSPrefix + 'column-header',
23     proxyOffsets: [-4, -9],
24
25     constructor: function(headerCt){
26         this.headerCt = headerCt;
27         this.ddGroup = this.getDDGroup();
28         this.callParent([headerCt.el]);
29     },
30
31     getDDGroup: function() {
32         return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id;
33     },
34
35     getTargetFromEvent : function(e){
36         return e.getTarget('.' + this.colHeaderCls);
37     },
38
39     getTopIndicator: function() {
40         if (!this.topIndicator) {
41             this.topIndicator = Ext.DomHelper.append(Ext.getBody(), {
42                 cls: "col-move-top",
43                 html: " "
44             }, true);
45         }
46         return this.topIndicator;
47     },
48
49     getBottomIndicator: function() {
50         if (!this.bottomIndicator) {
51             this.bottomIndicator = Ext.DomHelper.append(Ext.getBody(), {
52                 cls: "col-move-bottom",
53                 html: " "
54             }, true);
55         }
56         return this.bottomIndicator;
57     },
58
59     getLocation: function(e, t) {
60         var x      = e.getXY()[0],
61             region = Ext.fly(t).getRegion(),
62             pos, header;
63
64         if ((region.right - x) <= (region.right - region.left) / 2) {
65             pos = "after";
66         } else {
67             pos = "before";
68         }
69         return {
70             pos: pos,
71             header: Ext.getCmp(t.id),
72             node: t
73         };
74     },
75
76     positionIndicator: function(draggedHeader, node, e){
77         var location = this.getLocation(e, node),
78             header = location.header,
79             pos    = location.pos,
80             nextHd = draggedHeader.nextSibling('gridcolumn:not([hidden])'),
81             prevHd = draggedHeader.previousSibling('gridcolumn:not([hidden])'),
82             region, topIndicator, bottomIndicator, topAnchor, bottomAnchor,
83             topXY, bottomXY, headerCtEl, minX, maxX;
84
85         // Cannot drag beyond non-draggable start column
86         if (!header.draggable && header.getIndex() == 0) {
87             return false;
88         }
89
90         this.lastLocation = location;
91
92         if ((draggedHeader !== header) &&
93             ((pos === "before" && nextHd !== header) ||
94             (pos === "after" && prevHd !== header)) &&
95             !header.isDescendantOf(draggedHeader)) {
96
97             // As we move in between different DropZones that are in the same
98             // group (such as the case when in a locked grid), invalidateDrop
99             // on the other dropZones.
100             var allDropZones = Ext.dd.DragDropManager.getRelated(this),
101                 ln = allDropZones.length,
102                 i  = 0,
103                 dropZone;
104
105             for (; i < ln; i++) {
106                 dropZone = allDropZones[i];
107                 if (dropZone !== this && dropZone.invalidateDrop) {
108                     dropZone.invalidateDrop();
109                 }
110             }
111
112
113             this.valid = true;
114             topIndicator = this.getTopIndicator();
115             bottomIndicator = this.getBottomIndicator();
116             if (pos === 'before') {
117                 topAnchor = 'tl';
118                 bottomAnchor = 'bl';
119             } else {
120                 topAnchor = 'tr';
121                 bottomAnchor = 'br';
122             }
123             topXY = header.el.getAnchorXY(topAnchor);
124             bottomXY = header.el.getAnchorXY(bottomAnchor);
125
126             // constrain the indicators to the viewable section
127             headerCtEl = this.headerCt.el;
128             minX = headerCtEl.getLeft();
129             maxX = headerCtEl.getRight();
130
131             topXY[0] = Ext.Number.constrain(topXY[0], minX, maxX);
132             bottomXY[0] = Ext.Number.constrain(bottomXY[0], minX, maxX);
133
134             // adjust by offsets, this is to center the arrows so that they point
135             // at the split point
136             topXY[0] -= 4;
137             topXY[1] -= 9;
138             bottomXY[0] -= 4;
139
140             // position and show indicators
141             topIndicator.setXY(topXY);
142             bottomIndicator.setXY(bottomXY);
143             topIndicator.show();
144             bottomIndicator.show();
145         // invalidate drop operation and hide indicators
146         } else {
147             this.invalidateDrop();
148         }
149     },
150
151     invalidateDrop: function() {
152         this.valid = false;
153         this.hideIndicators();
154     },
155
156     onNodeOver: function(node, dragZone, e, data) {
157         if (data.header.el.dom !== node) {
158             this.positionIndicator(data.header, node, e);
159         }
160         return this.valid ? this.dropAllowed : this.dropNotAllowed;
161     },
162
163     hideIndicators: function() {
164         this.getTopIndicator().hide();
165         this.getBottomIndicator().hide();
166     },
167
168     onNodeOut: function() {
169         this.hideIndicators();
170     },
171
172     onNodeDrop: function(node, dragZone, e, data) {
173         if (this.valid) {
174             this.invalidateDrop();
175             var hd = data.header,
176                 lastLocation = this.lastLocation,
177                 fromCt  = hd.ownerCt,
178                 fromIdx = fromCt.items.indexOf(hd), // Container.items is a MixedCollection
179                 toCt    = lastLocation.header.ownerCt,
180                 toIdx   = toCt.items.indexOf(lastLocation.header),
181                 headerCt = this.headerCt,
182                 groupCt,
183                 scrollerOwner;
184
185             if (lastLocation.pos === 'after') {
186                 toIdx++;
187             }
188
189             // If we are dragging in between two HeaderContainers that have had the lockable
190             // mixin injected we will lock/unlock headers in between sections. Note that lockable
191             // does NOT currently support grouped headers.
192             if (fromCt !== toCt && fromCt.lockableInjected && toCt.lockableInjected && toCt.lockedCt) {
193                 scrollerOwner = fromCt.up('[scrollerOwner]');
194                 scrollerOwner.lock(hd, toIdx);
195             } else if (fromCt !== toCt && fromCt.lockableInjected && toCt.lockableInjected && fromCt.lockedCt) {
196                 scrollerOwner = fromCt.up('[scrollerOwner]');
197                 scrollerOwner.unlock(hd, toIdx);
198             } else {
199                 // If dragging rightwards, then after removal, the insertion index will be one less when moving
200                 // in between the same container.
201                 if ((fromCt === toCt) && (toIdx > fromCt.items.indexOf(hd))) {
202                     toIdx--;
203                 }
204
205                 // Remove dragged header from where it was without destroying it or relaying its Container
206                 if (fromCt !== toCt) {
207                     fromCt.suspendLayout = true;
208                     fromCt.remove(hd, false);
209                     fromCt.suspendLayout = false;
210                 }
211
212                 // Dragged the last header out of the fromCt group... The fromCt group must die
213                 if (fromCt.isGroupHeader) {
214                     if (!fromCt.items.getCount()) {
215                         groupCt = fromCt.ownerCt;
216                         groupCt.suspendLayout = true;
217                         groupCt.remove(fromCt, false);
218                         fromCt.el.dom.parentNode.removeChild(fromCt.el.dom);
219                         groupCt.suspendLayout = false;
220                     } else {
221                         fromCt.minWidth = fromCt.getWidth() - hd.getWidth();
222                         fromCt.setWidth(fromCt.minWidth);
223                     }
224                 }
225
226                 // Move dragged header into its drop position
227                 toCt.suspendLayout = true;
228                 if (fromCt === toCt) {
229                     toCt.move(fromIdx, toIdx);
230                 } else {
231                     toCt.insert(toIdx, hd);
232                 }
233                 toCt.suspendLayout = false;
234
235                 // Group headers acquire the aggregate width of their child headers
236                 // Therefore a child header may not flex; it must contribute a fixed width.
237                 // But we restore the flex value when moving back into the main header container
238                 if (toCt.isGroupHeader) {
239                     hd.savedFlex = hd.flex;
240                     delete hd.flex;
241                     hd.width = hd.getWidth();
242                     // When there was previously a flex, we need to ensure we don't count for the
243                     // border twice.
244                     toCt.minWidth = toCt.getWidth() + hd.getWidth() - (hd.savedFlex ? 1 : 0);
245                     toCt.setWidth(toCt.minWidth);
246                 } else {
247                     if (hd.savedFlex) {
248                         hd.flex = hd.savedFlex;
249                         delete hd.width;
250                     }
251                 }
252
253
254                 // Refresh columns cache in case we remove an emptied group column
255                 headerCt.purgeCache();
256                 headerCt.doLayout();
257                 headerCt.onHeaderMoved(hd, fromIdx, toIdx);
258                 // Emptied group header can only be destroyed after the header and grid have been refreshed
259                 if (!fromCt.items.getCount()) {
260                     fromCt.destroy();
261                 }
262             }
263         }
264     }
265 });
266