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