Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / layout / container / Accordion.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.layout.container.Accordion
17  * @extends Ext.layout.container.VBox
18  *
19  * This is a layout that manages multiple Panels in an expandable accordion style such that only
20  * **one Panel can be expanded at any given time**. Each Panel has built-in support for expanding and collapsing.
21  *
22  * Note: Only Ext Panels and all subclasses of Ext.panel.Panel may be used in an accordion layout Container.
23  *
24  *     @example
25  *     Ext.create('Ext.panel.Panel', {
26  *         title: 'Accordion Layout',
27  *         width: 300,
28  *         height: 300,
29  *         layout:'accordion',
30  *         defaults: {
31  *             // applied to each contained panel
32  *             bodyStyle: 'padding:15px'
33  *         },
34  *         layoutConfig: {
35  *             // layout-specific configs go here
36  *             titleCollapse: false,
37  *             animate: true,
38  *             activeOnTop: true
39  *         },
40  *         items: [{
41  *             title: 'Panel 1',
42  *             html: 'Panel content!'
43  *         },{
44  *             title: 'Panel 2',
45  *             html: 'Panel content!'
46  *         },{
47  *             title: 'Panel 3',
48  *             html: 'Panel content!'
49  *         }],
50  *         renderTo: Ext.getBody()
51  *     });
52  */
53 Ext.define('Ext.layout.container.Accordion', {
54     extend: 'Ext.layout.container.VBox',
55     alias: ['layout.accordion'],
56     alternateClassName: 'Ext.layout.AccordionLayout',
57
58     itemCls: Ext.baseCSSPrefix + 'box-item ' + Ext.baseCSSPrefix + 'accordion-item',
59
60     align: 'stretch',
61
62     /**
63      * @cfg {Boolean} fill
64      * True to adjust the active item's height to fill the available space in the container, false to use the
65      * item's current height, or auto height if not explicitly set.
66      */
67     fill : true,
68
69     /**
70      * @cfg {Boolean} autoWidth
71      * Child Panels have their width actively managed to fit within the accordion's width.
72      * @deprecated This config is ignored in ExtJS 4
73      */
74     autoWidth : true,
75
76     /**
77      * @cfg {Boolean} titleCollapse
78      * True to allow expand/collapse of each contained panel by clicking anywhere on the title bar, false to allow
79      * expand/collapse only when the toggle tool button is clicked.  When set to false,
80      * {@link #hideCollapseTool} should be false also.
81      */
82     titleCollapse : true,
83
84     /**
85      * @cfg {Boolean} hideCollapseTool
86      * True to hide the contained Panels' collapse/expand toggle buttons, false to display them.
87      * When set to true, {@link #titleCollapse} is automatically set to <code>true</code>.
88      */
89     hideCollapseTool : false,
90
91     /**
92      * @cfg {Boolean} collapseFirst
93      * True to make sure the collapse/expand toggle button always renders first (to the left of) any other tools
94      * in the contained Panels' title bars, false to render it last.
95      */
96     collapseFirst : false,
97
98     /**
99      * @cfg {Boolean} animate
100      * True to slide the contained panels open and closed during expand/collapse using animation, false to open and
101      * close directly with no animation. Note: The layout performs animated collapsing
102      * and expanding, <i>not</i> the child Panels.
103      */
104     animate : true,
105     /**
106      * @cfg {Boolean} activeOnTop
107      * Only valid when {@link #multi} is `false` and {@link #animate} is `false`.
108      *
109      * True to swap the position of each panel as it is expanded so that it becomes the first item in the container,
110      * false to keep the panels in the rendered order.
111      */
112     activeOnTop : false,
113     /**
114      * @cfg {Boolean} multi
115      * Set to <code>true</code> to enable multiple accordion items to be open at once.
116      */
117     multi: false,
118
119     constructor: function() {
120         var me = this;
121
122         me.callParent(arguments);
123
124         // animate flag must be false during initial render phase so we don't get animations.
125         me.initialAnimate = me.animate;
126         me.animate = false;
127
128         // Child Panels are not absolutely positioned if we are not filling, so use a different itemCls.
129         if (me.fill === false) {
130             me.itemCls = Ext.baseCSSPrefix + 'accordion-item';
131         }
132     },
133
134     // Cannot lay out a fitting accordion before we have been allocated a height.
135     // So during render phase, layout will not be performed.
136     beforeLayout: function() {
137         var me = this;
138
139         me.callParent(arguments);
140         if (me.fill) {
141             if (!(me.owner.el.dom.style.height || me.getLayoutTargetSize().height)) {
142                 return false;
143             }
144         } else {
145             me.owner.componentLayout.monitorChildren = false;
146             me.autoSize = true;
147             me.owner.setAutoScroll(true);
148         }
149     },
150
151     renderItems : function(items, target) {
152         var me = this,
153             ln = items.length,
154             i = 0,
155             comp,
156             targetSize = me.getLayoutTargetSize(),
157             renderedPanels = [];
158
159         for (; i < ln; i++) {
160             comp = items[i];
161             if (!comp.rendered) {
162                 renderedPanels.push(comp);
163
164                 // Set up initial properties for Panels in an accordion.
165                 if (me.collapseFirst) {
166                     comp.collapseFirst = me.collapseFirst;
167                 }
168                 if (me.hideCollapseTool) {
169                     comp.hideCollapseTool = me.hideCollapseTool;
170                     comp.titleCollapse = true;
171                 }
172                 else if (me.titleCollapse) {
173                     comp.titleCollapse = me.titleCollapse;
174                 }
175
176                 delete comp.hideHeader;
177                 comp.collapsible = true;
178                 comp.title = comp.title || '&#160;';
179
180                 // Set initial sizes
181                 comp.width = targetSize.width;
182                 if (me.fill) {
183                     delete comp.height;
184                     delete comp.flex;
185
186                     // If there is an expanded item, all others must be rendered collapsed.
187                     if (me.expandedItem !== undefined) {
188                         comp.collapsed = true;
189                     }
190                     // Otherwise expand the first item with collapsed explicitly configured as false
191                     else if (comp.hasOwnProperty('collapsed') && comp.collapsed === false) {
192                         comp.flex = 1;
193                         me.expandedItem = i;
194                     } else {
195                         comp.collapsed = true;
196                     }
197                     // If we are fitting, then intercept expand/collapse requests.
198                     me.owner.mon(comp, {
199                         show: me.onComponentShow,
200                         beforeexpand: me.onComponentExpand,
201                         beforecollapse: me.onComponentCollapse,
202                         scope: me
203                     });
204                 } else {
205                     delete comp.flex;
206                     comp.animCollapse = me.initialAnimate;
207                     comp.autoHeight = true;
208                     comp.autoScroll = false;
209                 }
210                 comp.border = comp.collapsed;
211             }
212         }
213
214         // If no collapsed:false Panels found, make the first one expanded.
215         if (ln && me.expandedItem === undefined) {
216             me.expandedItem = 0;
217             comp = items[0];
218             comp.collapsed = comp.border = false;
219             if (me.fill) {
220                 comp.flex = 1;
221             }
222         }
223
224         // Render all Panels.
225         me.callParent(arguments);
226
227         // Postprocess rendered Panels.
228         ln = renderedPanels.length;
229         for (i = 0; i < ln; i++) {
230             comp = renderedPanels[i];
231
232             // Delete the dimension property so that our align: 'stretch' processing manages the width from here
233             delete comp.width;
234
235             comp.header.addCls(Ext.baseCSSPrefix + 'accordion-hd');
236             comp.body.addCls(Ext.baseCSSPrefix + 'accordion-body');
237         }
238     },
239
240     onLayout: function() {
241         var me = this;
242
243
244         if (me.fill) {
245             me.callParent(arguments);
246         } else {
247             var targetSize = me.getLayoutTargetSize(),
248                 items = me.getVisibleItems(),
249                 len = items.length,
250                 i = 0, comp;
251
252             for (; i < len; i++) {
253                 comp = items[i];
254                 if (comp.collapsed) {
255                     items[i].setWidth(targetSize.width);
256                 } else {
257                     items[i].setSize(null, null);
258                 }
259             }
260         }
261         me.updatePanelClasses();
262
263         return me;
264     },
265
266     updatePanelClasses: function() {
267         var children = this.getLayoutItems(),
268             ln = children.length,
269             siblingCollapsed = true,
270             i, child;
271
272         for (i = 0; i < ln; i++) {
273             child = children[i];
274
275             // Fix for EXTJSIV-3724. Windows only.
276             // Collapsing the Psnel's el to a size which only allows a single hesder to be visible, scrolls the header out of view.
277             if (Ext.isWindows) {
278                 child.el.dom.scrollTop = 0;
279             }
280
281             if (siblingCollapsed) {
282                 child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
283             }
284             else {
285                 child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
286             }
287
288             if (i + 1 == ln && child.collapsed) {
289                 child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
290             }
291             else {
292                 child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
293             }
294             siblingCollapsed = child.collapsed;
295         }
296     },
297     
298     animCallback: function(){
299         Ext.Array.forEach(this.toCollapse, function(comp){
300             comp.fireEvent('collapse', comp);
301         });
302         
303         Ext.Array.forEach(this.toExpand, function(comp){
304             comp.fireEvent('expand', comp);
305         });    
306     },
307     
308     setupEvents: function(){
309         this.toCollapse = [];
310         this.toExpand = [];    
311     },
312
313     // When a Component expands, adjust the heights of the other Components to be just enough to accommodate
314     // their headers.
315     // The expanded Component receives the only flex value, and so gets all remaining space.
316     onComponentExpand: function(toExpand) {
317         var me = this,
318             it = me.owner.items.items,
319             len = it.length,
320             i = 0,
321             comp;
322
323         me.setupEvents();
324         for (; i < len; i++) {
325             comp = it[i];
326             if (comp === toExpand && comp.collapsed) {
327                 me.setExpanded(comp);
328             } else if (!me.multi && (comp.rendered && comp.header.rendered && comp !== toExpand && !comp.collapsed)) {
329                 me.setCollapsed(comp);
330             }
331         }
332
333         me.animate = me.initialAnimate;
334         if (me.activeOnTop) {
335             // insert will trigger a layout
336             me.owner.insert(0, toExpand); 
337         } else {
338             me.layout();
339         }
340         me.animate = false;
341         return false;
342     },
343
344     onComponentCollapse: function(comp) {
345         var me = this,
346             toExpand = comp.next() || comp.prev(),
347             expanded = me.multi ? me.owner.query('>panel:not([collapsed])') : [];
348
349         me.setupEvents();
350         // If we are allowing multi, and the "toCollapse" component is NOT the only expanded Component,
351         // then ask the box layout to collapse it to its header.
352         if (me.multi) {
353             me.setCollapsed(comp);
354
355             // If the collapsing Panel is the only expanded one, expand the following Component.
356             // All this is handling fill: true, so there must be at least one expanded,
357             if (expanded.length === 1 && expanded[0] === comp) {
358                 me.setExpanded(toExpand);
359             }
360
361             me.animate = me.initialAnimate;
362             me.layout();
363             me.animate = false;
364         }
365         // Not allowing multi: expand the next sibling if possible, prev sibling if we collapsed the last
366         else if (toExpand) {
367             me.onComponentExpand(toExpand);
368         }
369         return false;
370     },
371
372     onComponentShow: function(comp) {
373         // Showing a Component means that you want to see it, so expand it.
374         this.onComponentExpand(comp);
375     },
376
377     setCollapsed: function(comp) {
378         var otherDocks = comp.getDockedItems(),
379             dockItem,
380             len = otherDocks.length,
381             i = 0;
382
383         // Hide all docked items except the header
384         comp.hiddenDocked = [];
385         for (; i < len; i++) {
386             dockItem = otherDocks[i];
387             if ((dockItem !== comp.header) && !dockItem.hidden) {
388                 dockItem.hidden = true;
389                 comp.hiddenDocked.push(dockItem);
390             }
391         }
392         comp.addCls(comp.collapsedCls);
393         comp.header.addCls(comp.collapsedHeaderCls);
394         comp.height = comp.header.getHeight();
395         comp.el.setHeight(comp.height);
396         comp.collapsed = true;
397         delete comp.flex;
398         if (this.initialAnimate) {
399             this.toCollapse.push(comp);
400         } else {
401             comp.fireEvent('collapse', comp);
402         }
403         if (comp.collapseTool) {
404             comp.collapseTool.setType('expand-' + comp.getOppositeDirection(comp.collapseDirection));
405         }
406     },
407
408     setExpanded: function(comp) {
409         var otherDocks = comp.hiddenDocked,
410             len = otherDocks ? otherDocks.length : 0,
411             i = 0;
412
413         // Show temporarily hidden docked items
414         for (; i < len; i++) {
415             otherDocks[i].show();
416         }
417
418         // If it was an initial native collapse which hides the body
419         if (!comp.body.isVisible()) {
420             comp.body.show();
421         }
422         delete comp.collapsed;
423         delete comp.height;
424         delete comp.componentLayout.lastComponentSize;
425         comp.suspendLayout = false;
426         comp.flex = 1;
427         comp.removeCls(comp.collapsedCls);
428         comp.header.removeCls(comp.collapsedHeaderCls);
429          if (this.initialAnimate) {
430             this.toExpand.push(comp);
431         } else {
432             comp.fireEvent('expand', comp);
433         }
434         if (comp.collapseTool) {
435             comp.collapseTool.setType('collapse-' + comp.collapseDirection);
436         }
437         comp.setAutoScroll(comp.initialConfig.autoScroll);
438     }
439 });