Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / tree / ViewDropZone.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.tree.ViewDropZone
17  * @extends Ext.view.DropZone
18  * @private
19  */
20 Ext.define('Ext.tree.ViewDropZone', {
21     extend: 'Ext.view.DropZone',
22
23     /**
24      * @cfg {Boolean} allowParentInsert
25      * Allow inserting a dragged node between an expanded parent node and its first child that will become a
26      * sibling of the parent when dropped.
27      */
28     allowParentInserts: false,
29  
30     /**
31      * @cfg {String} allowContainerDrop
32      * True if drops on the tree container (outside of a specific tree node) are allowed.
33      */
34     allowContainerDrops: false,
35
36     /**
37      * @cfg {String} appendOnly
38      * True if the tree should only allow append drops (use for trees which are sorted).
39      */
40     appendOnly: false,
41
42     /**
43      * @cfg {String} expandDelay
44      * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node
45      * over the target.
46      */
47     expandDelay : 500,
48
49     indicatorCls: 'x-tree-ddindicator',
50
51     // private
52     expandNode : function(node) {
53         var view = this.view;
54         if (!node.isLeaf() && !node.isExpanded()) {
55             view.expand(node);
56             this.expandProcId = false;
57         }
58     },
59
60     // private
61     queueExpand : function(node) {
62         this.expandProcId = Ext.Function.defer(this.expandNode, this.expandDelay, this, [node]);
63     },
64
65     // private
66     cancelExpand : function() {
67         if (this.expandProcId) {
68             clearTimeout(this.expandProcId);
69             this.expandProcId = false;
70         }
71     },
72
73     getPosition: function(e, node) {
74         var view = this.view,
75             record = view.getRecord(node),
76             y = e.getPageY(),
77             noAppend = record.isLeaf(),
78             noBelow = false,
79             region = Ext.fly(node).getRegion(),
80             fragment;
81
82         // If we are dragging on top of the root node of the tree, we always want to append.
83         if (record.isRoot()) {
84             return 'append';
85         }
86
87         // Return 'append' if the node we are dragging on top of is not a leaf else return false.
88         if (this.appendOnly) {
89             return noAppend ? false : 'append';
90         }
91
92         if (!this.allowParentInsert) {
93             noBelow = record.hasChildNodes() && record.isExpanded();
94         }
95
96         fragment = (region.bottom - region.top) / (noAppend ? 2 : 3);
97         if (y >= region.top && y < (region.top + fragment)) {
98             return 'before';
99         }
100         else if (!noBelow && (noAppend || (y >= (region.bottom - fragment) && y <= region.bottom))) {
101             return 'after';
102         }
103         else {
104             return 'append';
105         }
106     },
107
108     isValidDropPoint : function(node, position, dragZone, e, data) {
109         if (!node || !data.item) {
110             return false;
111         }
112
113         var view = this.view,
114             targetNode = view.getRecord(node),
115             draggedRecords = data.records,
116             dataLength = draggedRecords.length,
117             ln = draggedRecords.length,
118             i, record;
119
120         // No drop position, or dragged records: invalid drop point
121         if (!(targetNode && position && dataLength)) {
122             return false;
123         }
124
125         // If the targetNode is within the folder we are dragging
126         for (i = 0; i < ln; i++) {
127             record = draggedRecords[i];
128             if (record.isNode && record.contains(targetNode)) {
129                 return false;
130             }
131         }
132         
133         // Respect the allowDrop field on Tree nodes
134         if (position === 'append' && targetNode.get('allowDrop') === false) {
135             return false;
136         }
137         else if (position != 'append' && targetNode.parentNode.get('allowDrop') === false) {
138             return false;
139         }
140
141         // If the target record is in the dragged dataset, then invalid drop
142         if (Ext.Array.contains(draggedRecords, targetNode)) {
143              return false;
144         }
145
146         // @TODO: fire some event to notify that there is a valid drop possible for the node you're dragging
147         // Yes: this.fireViewEvent(blah....) fires an event through the owning View.
148         return true;
149     },
150
151     onNodeOver : function(node, dragZone, e, data) {
152         var position = this.getPosition(e, node),
153             returnCls = this.dropNotAllowed,
154             view = this.view,
155             targetNode = view.getRecord(node),
156             indicator = this.getIndicator(),
157             indicatorX = 0,
158             indicatorY = 0;
159
160         // auto node expand check
161         this.cancelExpand();
162         if (position == 'append' && !this.expandProcId && !Ext.Array.contains(data.records, targetNode) && !targetNode.isLeaf() && !targetNode.isExpanded()) {
163             this.queueExpand(targetNode);
164         }
165             
166             
167         if (this.isValidDropPoint(node, position, dragZone, e, data)) {
168             this.valid = true;
169             this.currentPosition = position;
170             this.overRecord = targetNode;
171
172             indicator.setWidth(Ext.fly(node).getWidth());
173             indicatorY = Ext.fly(node).getY() - Ext.fly(view.el).getY() - 1;
174
175             /*
176              * In the code below we show the proxy again. The reason for doing this is showing the indicator will
177              * call toFront, causing it to get a new z-index which can sometimes push the proxy behind it. We always 
178              * want the proxy to be above, so calling show on the proxy will call toFront and bring it forward.
179              */
180             if (position == 'before') {
181                 returnCls = targetNode.isFirst() ? Ext.baseCSSPrefix + 'tree-drop-ok-above' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
182                 indicator.showAt(0, indicatorY);
183                 dragZone.proxy.show();
184             } else if (position == 'after') {
185                 returnCls = targetNode.isLast() ? Ext.baseCSSPrefix + 'tree-drop-ok-below' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
186                 indicatorY += Ext.fly(node).getHeight();
187                 indicator.showAt(0, indicatorY);
188                 dragZone.proxy.show();
189             } else {
190                 returnCls = Ext.baseCSSPrefix + 'tree-drop-ok-append';
191                 // @TODO: set a class on the parent folder node to be able to style it
192                 indicator.hide();
193             }
194         } else {
195             this.valid = false;
196         }
197
198         this.currentCls = returnCls;
199         return returnCls;
200     },
201
202     onContainerOver : function(dd, e, data) {
203         return e.getTarget('.' + this.indicatorCls) ? this.currentCls : this.dropNotAllowed;
204     },
205     
206     notifyOut: function() {
207         this.callParent(arguments);
208         this.cancelExpand();
209     },
210
211     handleNodeDrop : function(data, targetNode, position) {
212         var me = this,
213             view = me.view,
214             parentNode = targetNode.parentNode,
215             store = view.getStore(),
216             recordDomNodes = [],
217             records, i, len,
218             insertionMethod, argList,
219             needTargetExpand,
220             transferData,
221             processDrop;
222
223         // If the copy flag is set, create a copy of the Models with the same IDs
224         if (data.copy) {
225             records = data.records;
226             data.records = [];
227             for (i = 0, len = records.length; i < len; i++) {
228                 data.records.push(Ext.apply({}, records[i].data));
229             }
230         }
231
232         // Cancel any pending expand operation
233         me.cancelExpand();
234
235         // Grab a reference to the correct node insertion method.
236         // Create an arg list array intended for the apply method of the
237         // chosen node insertion method.
238         // Ensure the target object for the method is referenced by 'targetNode'
239         if (position == 'before') {
240             insertionMethod = parentNode.insertBefore;
241             argList = [null, targetNode];
242             targetNode = parentNode;
243         }
244         else if (position == 'after') {
245             if (targetNode.nextSibling) {
246                 insertionMethod = parentNode.insertBefore;
247                 argList = [null, targetNode.nextSibling];
248             }
249             else {
250                 insertionMethod = parentNode.appendChild;
251                 argList = [null];
252             }
253             targetNode = parentNode;
254         }
255         else {
256             if (!targetNode.isExpanded()) {
257                 needTargetExpand = true;
258             }
259             insertionMethod = targetNode.appendChild;
260             argList = [null];
261         }
262
263         // A function to transfer the data into the destination tree
264         transferData = function() {
265             var node;
266             for (i = 0, len = data.records.length; i < len; i++) {
267                 argList[0] = data.records[i];
268                 node = insertionMethod.apply(targetNode, argList);
269                 
270                 if (Ext.enableFx && me.dropHighlight) {
271                     recordDomNodes.push(view.getNode(node));
272                 }
273             }
274             
275             // Kick off highlights after everything's been inserted, so they are
276             // more in sync without insertion/render overhead.
277             if (Ext.enableFx && me.dropHighlight) {
278                 //FIXME: the check for n.firstChild is not a great solution here. Ideally the line should simply read 
279                 //Ext.fly(n.firstChild) but this yields errors in IE6 and 7. See ticket EXTJSIV-1705 for more details
280                 Ext.Array.forEach(recordDomNodes, function(n) {
281                     if (n) {
282                         Ext.fly(n.firstChild ? n.firstChild : n).highlight(me.dropHighlightColor);
283                     }
284                 });
285             }
286         };
287
288         // If dropping right on an unexpanded node, transfer the data after it is expanded.
289         if (needTargetExpand) {
290             targetNode.expand(false, transferData);
291         }
292         // Otherwise, call the data transfer function immediately
293         else {
294             transferData();
295         }
296     }
297 });