Upgrade to ExtJS 3.2.0 - Released 03/30/2010
[extjs.git] / src / widgets / tree / TreeDropZone.js
1 /*!
2  * Ext JS Library 3.2.0
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.tree.TreeDropZone
9  * @extends Ext.dd.DropZone
10  * @constructor
11  * @param {String/HTMLElement/Element} tree The {@link Ext.tree.TreePanel} for which to enable dropping
12  * @param {Object} config
13  */
14 if(Ext.dd.DropZone){
15     
16 Ext.tree.TreeDropZone = function(tree, config){
17     /**
18      * @cfg {Boolean} allowParentInsert
19      * Allow inserting a dragged node between an expanded parent node and its first child that will become a
20      * sibling of the parent when dropped (defaults to false)
21      */
22     this.allowParentInsert = config.allowParentInsert || false;
23     /**
24      * @cfg {String} allowContainerDrop
25      * True if drops on the tree container (outside of a specific tree node) are allowed (defaults to false)
26      */
27     this.allowContainerDrop = config.allowContainerDrop || false;
28     /**
29      * @cfg {String} appendOnly
30      * True if the tree should only allow append drops (use for trees which are sorted, defaults to false)
31      */
32     this.appendOnly = config.appendOnly || false;
33
34     Ext.tree.TreeDropZone.superclass.constructor.call(this, tree.getTreeEl(), config);
35     /**
36     * The TreePanel for this drop zone
37     * @type Ext.tree.TreePanel
38     * @property
39     */
40     this.tree = tree;
41     /**
42     * Arbitrary data that can be associated with this tree and will be included in the event object that gets
43     * passed to any nodedragover event handler (defaults to {})
44     * @type Ext.tree.TreePanel
45     * @property
46     */
47     this.dragOverData = {};
48     // private
49     this.lastInsertClass = "x-tree-no-status";
50 };
51
52 Ext.extend(Ext.tree.TreeDropZone, Ext.dd.DropZone, {
53     /**
54      * @cfg {String} ddGroup
55      * A named drag drop group to which this object belongs.  If a group is specified, then this object will only
56      * interact with other drag drop objects in the same group (defaults to 'TreeDD').
57      */
58     ddGroup : "TreeDD",
59
60     /**
61      * @cfg {String} expandDelay
62      * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node
63      * over the target (defaults to 1000)
64      */
65     expandDelay : 1000,
66
67     // private
68     expandNode : function(node){
69         if(node.hasChildNodes() && !node.isExpanded()){
70             node.expand(false, null, this.triggerCacheRefresh.createDelegate(this));
71         }
72     },
73
74     // private
75     queueExpand : function(node){
76         this.expandProcId = this.expandNode.defer(this.expandDelay, this, [node]);
77     },
78
79     // private
80     cancelExpand : function(){
81         if(this.expandProcId){
82             clearTimeout(this.expandProcId);
83             this.expandProcId = false;
84         }
85     },
86
87     // private
88     isValidDropPoint : function(n, pt, dd, e, data){
89         if(!n || !data){ return false; }
90         var targetNode = n.node;
91         var dropNode = data.node;
92         // default drop rules
93         if(!(targetNode && targetNode.isTarget && pt)){
94             return false;
95         }
96         if(pt == "append" && targetNode.allowChildren === false){
97             return false;
98         }
99         if((pt == "above" || pt == "below") && (targetNode.parentNode && targetNode.parentNode.allowChildren === false)){
100             return false;
101         }
102         if(dropNode && (targetNode == dropNode || dropNode.contains(targetNode))){
103             return false;
104         }
105         // reuse the object
106         var overEvent = this.dragOverData;
107         overEvent.tree = this.tree;
108         overEvent.target = targetNode;
109         overEvent.data = data;
110         overEvent.point = pt;
111         overEvent.source = dd;
112         overEvent.rawEvent = e;
113         overEvent.dropNode = dropNode;
114         overEvent.cancel = false;  
115         var result = this.tree.fireEvent("nodedragover", overEvent);
116         return overEvent.cancel === false && result !== false;
117     },
118
119     // private
120     getDropPoint : function(e, n, dd){
121         var tn = n.node;
122         if(tn.isRoot){
123             return tn.allowChildren !== false ? "append" : false; // always append for root
124         }
125         var dragEl = n.ddel;
126         var t = Ext.lib.Dom.getY(dragEl), b = t + dragEl.offsetHeight;
127         var y = Ext.lib.Event.getPageY(e);
128         var noAppend = tn.allowChildren === false || tn.isLeaf();
129         if(this.appendOnly || tn.parentNode.allowChildren === false){
130             return noAppend ? false : "append";
131         }
132         var noBelow = false;
133         if(!this.allowParentInsert){
134             noBelow = tn.hasChildNodes() && tn.isExpanded();
135         }
136         var q = (b - t) / (noAppend ? 2 : 3);
137         if(y >= t && y < (t + q)){
138             return "above";
139         }else if(!noBelow && (noAppend || y >= b-q && y <= b)){
140             return "below";
141         }else{
142             return "append";
143         }
144     },
145
146     // private
147     onNodeEnter : function(n, dd, e, data){
148         this.cancelExpand();
149     },
150     
151     onContainerOver : function(dd, e, data) {
152         if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) {
153             return this.dropAllowed;
154         }
155         return this.dropNotAllowed;
156     },
157
158     // private
159     onNodeOver : function(n, dd, e, data){
160         var pt = this.getDropPoint(e, n, dd);
161         var node = n.node;
162         
163         // auto node expand check
164         if(!this.expandProcId && pt == "append" && node.hasChildNodes() && !n.node.isExpanded()){
165             this.queueExpand(node);
166         }else if(pt != "append"){
167             this.cancelExpand();
168         }
169         
170         // set the insert point style on the target node
171         var returnCls = this.dropNotAllowed;
172         if(this.isValidDropPoint(n, pt, dd, e, data)){
173            if(pt){
174                var el = n.ddel;
175                var cls;
176                if(pt == "above"){
177                    returnCls = n.node.isFirst() ? "x-tree-drop-ok-above" : "x-tree-drop-ok-between";
178                    cls = "x-tree-drag-insert-above";
179                }else if(pt == "below"){
180                    returnCls = n.node.isLast() ? "x-tree-drop-ok-below" : "x-tree-drop-ok-between";
181                    cls = "x-tree-drag-insert-below";
182                }else{
183                    returnCls = "x-tree-drop-ok-append";
184                    cls = "x-tree-drag-append";
185                }
186                if(this.lastInsertClass != cls){
187                    Ext.fly(el).replaceClass(this.lastInsertClass, cls);
188                    this.lastInsertClass = cls;
189                }
190            }
191        }
192        return returnCls;
193     },
194
195     // private
196     onNodeOut : function(n, dd, e, data){
197         this.cancelExpand();
198         this.removeDropIndicators(n);
199     },
200
201     // private
202     onNodeDrop : function(n, dd, e, data){
203         var point = this.getDropPoint(e, n, dd);
204         var targetNode = n.node;
205         targetNode.ui.startDrop();
206         if(!this.isValidDropPoint(n, point, dd, e, data)){
207             targetNode.ui.endDrop();
208             return false;
209         }
210         // first try to find the drop node
211         var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, point, e) : null);
212         return this.processDrop(targetNode, data, point, dd, e, dropNode);
213     },
214     
215     onContainerDrop : function(dd, e, data){
216         if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) {
217             var targetNode = this.tree.getRootNode();       
218             targetNode.ui.startDrop();
219             var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, 'append', e) : null);
220             return this.processDrop(targetNode, data, 'append', dd, e, dropNode);
221         }
222         return false;
223     },
224     
225     // private
226     processDrop: function(target, data, point, dd, e, dropNode){
227         var dropEvent = {
228             tree : this.tree,
229             target: target,
230             data: data,
231             point: point,
232             source: dd,
233             rawEvent: e,
234             dropNode: dropNode,
235             cancel: !dropNode,
236             dropStatus: false
237         };
238         var retval = this.tree.fireEvent("beforenodedrop", dropEvent);
239         if(retval === false || dropEvent.cancel === true || !dropEvent.dropNode){
240             target.ui.endDrop();
241             return dropEvent.dropStatus;
242         }
243     
244         target = dropEvent.target;
245         if(point == 'append' && !target.isExpanded()){
246             target.expand(false, null, function(){
247                 this.completeDrop(dropEvent);
248             }.createDelegate(this));
249         }else{
250             this.completeDrop(dropEvent);
251         }
252         return true;
253     },
254
255     // private
256     completeDrop : function(de){
257         var ns = de.dropNode, p = de.point, t = de.target;
258         if(!Ext.isArray(ns)){
259             ns = [ns];
260         }
261         var n;
262         for(var i = 0, len = ns.length; i < len; i++){
263             n = ns[i];
264             if(p == "above"){
265                 t.parentNode.insertBefore(n, t);
266             }else if(p == "below"){
267                 t.parentNode.insertBefore(n, t.nextSibling);
268             }else{
269                 t.appendChild(n);
270             }
271         }
272         n.ui.focus();
273         if(Ext.enableFx && this.tree.hlDrop){
274             n.ui.highlight();
275         }
276         t.ui.endDrop();
277         this.tree.fireEvent("nodedrop", de);
278     },
279
280     // private
281     afterNodeMoved : function(dd, data, e, targetNode, dropNode){
282         if(Ext.enableFx && this.tree.hlDrop){
283             dropNode.ui.focus();
284             dropNode.ui.highlight();
285         }
286         this.tree.fireEvent("nodedrop", this.tree, targetNode, data, dd, e);
287     },
288
289     // private
290     getTree : function(){
291         return this.tree;
292     },
293
294     // private
295     removeDropIndicators : function(n){
296         if(n && n.ddel){
297             var el = n.ddel;
298             Ext.fly(el).removeClass([
299                     "x-tree-drag-insert-above",
300                     "x-tree-drag-insert-below",
301                     "x-tree-drag-append"]);
302             this.lastInsertClass = "_noclass";
303         }
304     },
305
306     // private
307     beforeDragDrop : function(target, e, id){
308         this.cancelExpand();
309         return true;
310     },
311
312     // private
313     afterRepair : function(data){
314         if(data && Ext.enableFx){
315             data.node.ui.highlight();
316         }
317         this.hideProxy();
318     }    
319 });
320
321 }