Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / tree / View.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.View
17  * @extends Ext.view.Table
18  */
19 Ext.define('Ext.tree.View', {
20     extend: 'Ext.view.Table',
21     alias: 'widget.treeview',
22
23     loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading',
24     expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded',
25
26     expanderSelector: '.' + Ext.baseCSSPrefix + 'tree-expander',
27     checkboxSelector: '.' + Ext.baseCSSPrefix + 'tree-checkbox',
28     expanderIconOverCls: Ext.baseCSSPrefix + 'tree-expander-over',
29
30     blockRefresh: true,
31
32     /** 
33      * @cfg {Boolean} rootVisible <tt>false</tt> to hide the root node (defaults to <tt>true</tt>)
34      */
35     rootVisible: true,
36
37     /** 
38      * @cfg {Boolean} animate <tt>true</tt> to enable animated expand/collapse (defaults to the value of {@link Ext#enableFx Ext.enableFx})
39      */
40
41     expandDuration: 250,
42     collapseDuration: 250,
43     
44     toggleOnDblClick: true,
45
46     initComponent: function() {
47         var me = this;
48         
49         if (me.initialConfig.animate === undefined) {
50             me.animate = Ext.enableFx;
51         }
52         
53         me.store = Ext.create('Ext.data.NodeStore', {
54             recursive: true,
55             rootVisible: me.rootVisible,
56             listeners: {
57                 beforeexpand: me.onBeforeExpand,
58                 expand: me.onExpand,
59                 beforecollapse: me.onBeforeCollapse,
60                 collapse: me.onCollapse,
61                 scope: me
62             }
63         });
64         
65         if (me.node) {
66             me.setRootNode(me.node);
67         }
68         me.animQueue = {};
69         me.callParent(arguments);
70     },
71     
72     onClear: function(){
73         this.store.removeAll();    
74     },
75
76     setRootNode: function(node) {
77         var me = this;        
78         me.store.setNode(node);
79         me.node = node;
80         if (!me.rootVisible) {
81             node.expand();
82         }
83     },
84     
85     onRender: function() {
86         var me = this,
87             opts = {delegate: me.expanderSelector},
88             el;
89
90         me.callParent(arguments);
91
92         el = me.el;
93         el.on({
94             scope: me,
95             delegate: me.expanderSelector,
96             mouseover: me.onExpanderMouseOver,
97             mouseout: me.onExpanderMouseOut
98         });
99         el.on({
100             scope: me,
101             delegate: me.checkboxSelector,
102             click: me.onCheckboxChange
103         });
104     },
105
106     onCheckboxChange: function(e, t) {
107         var item = e.getTarget(this.getItemSelector(), this.getTargetEl()),
108             record, value;
109             
110         if (item) {
111             record = this.getRecord(item);
112             value = !record.get('checked');
113             record.set('checked', value);
114             this.fireEvent('checkchange', record, value);
115         }
116     },
117
118     getChecked: function() {
119         var checked = [];
120         this.node.cascadeBy(function(rec){
121             if (rec.get('checked')) {
122                 checked.push(rec);
123             }
124         });
125         return checked;
126     },
127     
128     isItemChecked: function(rec){
129         return rec.get('checked');
130     },
131
132     createAnimWrap: function(record, index) {
133         var thHtml = '',
134             headerCt = this.panel.headerCt,
135             headers = headerCt.getGridColumns(),
136             i = 0, len = headers.length, item,
137             node = this.getNode(record),
138             tmpEl, nodeEl;
139
140         for (; i < len; i++) {
141             item = headers[i];
142             thHtml += '<th style="width: ' + (item.hidden ? 0 : item.getDesiredWidth()) + 'px; height: 0px;"></th>';
143         }
144
145         nodeEl = Ext.get(node);        
146         tmpEl = nodeEl.insertSibling({
147             tag: 'tr',
148             html: [
149                 '<td colspan="' + headerCt.getColumnCount() + '">',
150                     '<div class="' + Ext.baseCSSPrefix + 'tree-animator-wrap' + '">',
151                         '<table class="' + Ext.baseCSSPrefix + 'grid-table" style="width: ' + headerCt.getFullWidth() + 'px;"><tbody>',
152                             thHtml,
153                         '</tbody></table>',
154                     '</div>',
155                 '</td>'
156             ].join('')
157         }, 'after');
158
159         return {
160             record: record,
161             node: node,
162             el: tmpEl,
163             expanding: false,
164             collapsing: false,
165             animating: false,
166             animateEl: tmpEl.down('div'),
167             targetEl: tmpEl.down('tbody')
168         };
169     },
170
171     getAnimWrap: function(parent) {
172         if (!this.animate) {
173             return null;
174         }
175
176         // We are checking to see which parent is having the animation wrap
177         while (parent) {
178             if (parent.animWrap) {
179                 return parent.animWrap;
180             }
181             parent = parent.parentNode;
182         }
183         return null;
184     },
185
186     doAdd: function(nodes, records, index) {
187         // If we are adding records which have a parent that is currently expanding
188         // lets add them to the animation wrap
189         var me = this,
190             record = records[0],
191             parent = record.parentNode,
192             a = me.all.elements,
193             relativeIndex = 0,
194             animWrap = me.getAnimWrap(parent),
195             targetEl, children, len;
196
197         if (!animWrap || !animWrap.expanding) {
198             me.resetScrollers();
199             return me.callParent(arguments);
200         }
201
202         // We need the parent that has the animWrap, not the nodes parent
203         parent = animWrap.record;
204         
205         // If there is an anim wrap we do our special magic logic
206         targetEl = animWrap.targetEl;
207         children = targetEl.dom.childNodes;
208         
209         // We subtract 1 from the childrens length because we have a tr in there with the th'es
210         len = children.length - 1;
211         
212         // The relative index is the index in the full flat collection minus the index of the wraps parent
213         relativeIndex = index - me.indexOf(parent) - 1;
214         
215         // If we are adding records to the wrap that have a higher relative index then there are currently children
216         // it means we have to append the nodes to the wrap
217         if (!len || relativeIndex >= len) {
218             targetEl.appendChild(nodes);
219         }
220         // If there are already more children then the relative index it means we are adding child nodes of
221         // some expanded node in the anim wrap. In this case we have to insert the nodes in the right location
222         else {
223             // +1 because of the tr with th'es that is already there
224             Ext.fly(children[relativeIndex + 1]).insertSibling(nodes, 'before', true);
225         }
226
227         // We also have to update the CompositeElementLite collection of the DataView
228         Ext.Array.insert(a, index, nodes);
229         
230         // If we were in an animation we need to now change the animation
231         // because the targetEl just got higher.
232         if (animWrap.isAnimating) {
233             me.onExpand(parent);
234         }
235     },
236     
237     doRemove: function(record, index) {
238         // If we are adding records which have a parent that is currently expanding
239         // lets add them to the animation wrap
240         var me = this,
241             parent = record.parentNode,
242             all = me.all,
243             animWrap = me.getAnimWrap(record),
244             node = all.item(index).dom;
245
246         if (!animWrap || !animWrap.collapsing) {
247             me.resetScrollers();
248             return me.callParent(arguments);
249         }
250
251         animWrap.targetEl.appendChild(node);
252         all.removeElement(index);
253     },
254
255     onBeforeExpand: function(parent, records, index) {
256         var me = this,
257             animWrap;
258             
259         if (!me.rendered || !me.animate) {
260             return;
261         }
262
263         if (me.getNode(parent)) {
264             animWrap = me.getAnimWrap(parent);
265             if (!animWrap) {
266                 animWrap = parent.animWrap = me.createAnimWrap(parent);
267                 animWrap.animateEl.setHeight(0);
268             }
269             else if (animWrap.collapsing) {
270                 // If we expand this node while it is still expanding then we
271                 // have to remove the nodes from the animWrap.
272                 animWrap.targetEl.select(me.itemSelector).remove();
273             } 
274             animWrap.expanding = true;
275             animWrap.collapsing = false;
276         }
277     },
278
279     onExpand: function(parent) {
280         var me = this,
281             queue = me.animQueue,
282             id = parent.getId(),
283             animWrap,
284             animateEl, 
285             targetEl,
286             queueItem;        
287         
288         if (me.singleExpand) {
289             me.ensureSingleExpand(parent);
290         }
291         
292         animWrap = me.getAnimWrap(parent);
293
294         if (!animWrap) {
295             me.resetScrollers();
296             return;
297         }
298         
299         animateEl = animWrap.animateEl;
300         targetEl = animWrap.targetEl;
301
302         animateEl.stopAnimation();
303         // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0
304         queue[id] = true;
305         animateEl.slideIn('t', {
306             duration: me.expandDuration,
307             listeners: {
308                 scope: me,
309                 lastframe: function() {
310                     // Move all the nodes out of the anim wrap to their proper location
311                     animWrap.el.insertSibling(targetEl.query(me.itemSelector), 'before');
312                     animWrap.el.remove();
313                     me.resetScrollers();
314                     delete animWrap.record.animWrap;
315                     delete queue[id];
316                 }
317             }
318         });
319         
320         animWrap.isAnimating = true;
321     },
322     
323     resetScrollers: function(){
324         var panel = this.panel;
325         
326         panel.determineScrollbars();
327         panel.invalidateScroller();
328     },
329
330     onBeforeCollapse: function(parent, records, index) {
331         var me = this,
332             animWrap;
333             
334         if (!me.rendered || !me.animate) {
335             return;
336         }
337
338         if (me.getNode(parent)) {
339             animWrap = me.getAnimWrap(parent);
340             if (!animWrap) {
341                 animWrap = parent.animWrap = me.createAnimWrap(parent, index);
342             }
343             else if (animWrap.expanding) {
344                 // If we collapse this node while it is still expanding then we
345                 // have to remove the nodes from the animWrap.
346                 animWrap.targetEl.select(this.itemSelector).remove();
347             }
348             animWrap.expanding = false;
349             animWrap.collapsing = true;
350         }
351     },
352     
353     onCollapse: function(parent) {
354         var me = this,
355             queue = me.animQueue,
356             id = parent.getId(),
357             animWrap = me.getAnimWrap(parent),
358             animateEl, targetEl;
359
360         if (!animWrap) {
361             me.resetScrollers();
362             return;
363         }
364         
365         animateEl = animWrap.animateEl;
366         targetEl = animWrap.targetEl;
367
368         queue[id] = true;
369         
370         // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0
371         animateEl.stopAnimation();
372         animateEl.slideOut('t', {
373             duration: me.collapseDuration,
374             listeners: {
375                 scope: me,
376                 lastframe: function() {
377                     animWrap.el.remove();
378                     delete animWrap.record.animWrap;
379                     me.resetScrollers();
380                     delete queue[id];
381                 }             
382             }
383         });
384         animWrap.isAnimating = true;
385     },
386     
387     /**
388      * Checks if a node is currently undergoing animation
389      * @private
390      * @param {Ext.data.Model} node The node
391      * @return {Boolean} True if the node is animating
392      */
393     isAnimating: function(node) {
394         return !!this.animQueue[node.getId()];    
395     },
396     
397     collectData: function(records) {
398         var data = this.callParent(arguments),
399             rows = data.rows,
400             len = rows.length,
401             i = 0,
402             row, record;
403             
404         for (; i < len; i++) {
405             row = rows[i];
406             record = records[i];
407             if (record.get('qtip')) {
408                 row.rowAttr = 'data-qtip="' + record.get('qtip') + '"';
409                 if (record.get('qtitle')) {
410                     row.rowAttr += ' ' + 'data-qtitle="' + record.get('qtitle') + '"';
411                 }
412             }
413             if (record.isExpanded()) {
414                 row.rowCls = (row.rowCls || '') + ' ' + this.expandedCls;
415             }
416             if (record.isLoading()) {
417                 row.rowCls = (row.rowCls || '') + ' ' + this.loadingCls;
418             }
419         }
420         
421         return data;
422     },
423     
424     /**
425      * Expand a record that is loaded in the view.
426      * @param {Ext.data.Model} record The record to expand
427      * @param {Boolean} deep (optional) True to expand nodes all the way down the tree hierarchy.
428      * @param {Function} callback (optional) The function to run after the expand is completed
429      * @param {Object} scope (optional) The scope of the callback function.
430      */
431     expand: function(record, deep, callback, scope) {
432         return record.expand(deep, callback, scope);
433     },
434     
435     /**
436      * Collapse a record that is loaded in the view.
437      * @param {Ext.data.Model} record The record to collapse
438      * @param {Boolean} deep (optional) True to collapse nodes all the way up the tree hierarchy.
439      * @param {Function} callback (optional) The function to run after the collapse is completed
440      * @param {Object} scope (optional) The scope of the callback function.
441      */
442     collapse: function(record, deep, callback, scope) {
443         return record.collapse(deep, callback, scope);
444     },
445     
446     /**
447      * Toggle a record between expanded and collapsed.
448      * @param {Ext.data.Record} recordInstance
449      */
450     toggle: function(record) {
451         this[record.isExpanded() ? 'collapse' : 'expand'](record);
452     },
453     
454     onItemDblClick: function(record, item, index) {
455         this.callParent(arguments);
456         if (this.toggleOnDblClick) {
457             this.toggle(record);
458         }
459     },
460     
461     onBeforeItemMouseDown: function(record, item, index, e) {
462         if (e.getTarget(this.expanderSelector, item)) {
463             return false;
464         }
465         return this.callParent(arguments);
466     },
467     
468     onItemClick: function(record, item, index, e) {
469         if (e.getTarget(this.expanderSelector, item)) {
470             this.toggle(record);
471             return false;
472         }
473         return this.callParent(arguments);
474     },
475     
476     onExpanderMouseOver: function(e, t) {
477         e.getTarget(this.cellSelector, 10, true).addCls(this.expanderIconOverCls);
478     },
479     
480     onExpanderMouseOut: function(e, t) {
481         e.getTarget(this.cellSelector, 10, true).removeCls(this.expanderIconOverCls);
482     },
483     
484     /**
485      * Gets the base TreeStore from the bound TreePanel.
486      */
487     getTreeStore: function() {
488         return this.panel.store;
489     },    
490     
491     ensureSingleExpand: function(node) {
492         var parent = node.parentNode;
493         if (parent) {
494             parent.eachChild(function(child) {
495                 if (child !== node && child.isExpanded()) {
496                     child.collapse();
497                 }
498             });
499         }
500     }
501 });