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