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