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