/** * @class Ext.layout.container.Accordion * @extends Ext.layout.container.VBox * * This is a layout that manages multiple Panels in an expandable accordion style such that only * **one Panel can be expanded at any given time**. Each Panel has built-in support for expanding and collapsing. * * Note: Only Ext Panels and all subclasses of Ext.panel.Panel may be used in an accordion layout Container. * * @example * Ext.create('Ext.panel.Panel', { * title: 'Accordion Layout', * width: 300, * height: 300, * layout:'accordion', * defaults: { * // applied to each contained panel * bodyStyle: 'padding:15px' * }, * layoutConfig: { * // layout-specific configs go here * titleCollapse: false, * animate: true, * activeOnTop: true * }, * items: [{ * title: 'Panel 1', * html: 'Panel content!' * },{ * title: 'Panel 2', * html: 'Panel content!' * },{ * title: 'Panel 3', * html: 'Panel content!' * }], * renderTo: Ext.getBody() * }); */ Ext.define('Ext.layout.container.Accordion', { extend: 'Ext.layout.container.VBox', alias: ['layout.accordion'], alternateClassName: 'Ext.layout.AccordionLayout', itemCls: Ext.baseCSSPrefix + 'box-item ' + Ext.baseCSSPrefix + 'accordion-item', align: 'stretch', /** * @cfg {Boolean} fill * True to adjust the active item's height to fill the available space in the container, false to use the * item's current height, or auto height if not explicitly set. */ fill : true, /** * @cfg {Boolean} autoWidth * Child Panels have their width actively managed to fit within the accordion's width. * @deprecated This config is ignored in ExtJS 4 */ autoWidth : true, /** * @cfg {Boolean} titleCollapse * True to allow expand/collapse of each contained panel by clicking anywhere on the title bar, false to allow * expand/collapse only when the toggle tool button is clicked. When set to false, * {@link #hideCollapseTool} should be false also. */ titleCollapse : true, /** * @cfg {Boolean} hideCollapseTool * True to hide the contained Panels' collapse/expand toggle buttons, false to display them. * When set to true, {@link #titleCollapse} is automatically set to <code>true</code>. */ hideCollapseTool : false, /** * @cfg {Boolean} collapseFirst * True to make sure the collapse/expand toggle button always renders first (to the left of) any other tools * in the contained Panels' title bars, false to render it last. */ collapseFirst : false, /** * @cfg {Boolean} animate * True to slide the contained panels open and closed during expand/collapse using animation, false to open and * close directly with no animation. Note: The layout performs animated collapsing * and expanding, <i>not</i> the child Panels. */ animate : true, /** * @cfg {Boolean} activeOnTop * Only valid when {@link #multi} is `false` and {@link #animate} is `false`. * * True to swap the position of each panel as it is expanded so that it becomes the first item in the container, * false to keep the panels in the rendered order. */ activeOnTop : false, /** * @cfg {Boolean} multi * Set to <code>true</code> to enable multiple accordion items to be open at once. */ multi: false, constructor: function() { var me = this; me.callParent(arguments); // animate flag must be false during initial render phase so we don't get animations. me.initialAnimate = me.animate; me.animate = false; // Child Panels are not absolutely positioned if we are not filling, so use a different itemCls. if (me.fill === false) { me.itemCls = Ext.baseCSSPrefix + 'accordion-item'; } }, // Cannot lay out a fitting accordion before we have been allocated a height. // So during render phase, layout will not be performed. beforeLayout: function() { var me = this; me.callParent(arguments); if (me.fill) { if (!(me.owner.el.dom.style.height || me.getLayoutTargetSize().height)) { return false; } } else { me.owner.componentLayout.monitorChildren = false; me.autoSize = true; me.owner.setAutoScroll(true); } }, renderItems : function(items, target) { var me = this, ln = items.length, i = 0, comp, targetSize = me.getLayoutTargetSize(), renderedPanels = []; for (; i < ln; i++) { comp = items[i]; if (!comp.rendered) { renderedPanels.push(comp); // Set up initial properties for Panels in an accordion. if (me.collapseFirst) { comp.collapseFirst = me.collapseFirst; } if (me.hideCollapseTool) { comp.hideCollapseTool = me.hideCollapseTool; comp.titleCollapse = true; } else if (me.titleCollapse) { comp.titleCollapse = me.titleCollapse; } delete comp.hideHeader; comp.collapsible = true; comp.title = comp.title || ' '; // Set initial sizes comp.width = targetSize.width; if (me.fill) { delete comp.height; delete comp.flex; // If there is an expanded item, all others must be rendered collapsed. if (me.expandedItem !== undefined) { comp.collapsed = true; } // Otherwise expand the first item with collapsed explicitly configured as false else if (comp.hasOwnProperty('collapsed') && comp.collapsed === false) { comp.flex = 1; me.expandedItem = i; } else { comp.collapsed = true; } // If we are fitting, then intercept expand/collapse requests. me.owner.mon(comp, { show: me.onComponentShow, beforeexpand: me.onComponentExpand, beforecollapse: me.onComponentCollapse, scope: me }); } else { delete comp.flex; comp.animCollapse = me.initialAnimate; comp.autoHeight = true; comp.autoScroll = false; } comp.border = comp.collapsed; } } // If no collapsed:false Panels found, make the first one expanded. if (ln && me.expandedItem === undefined) { me.expandedItem = 0; comp = items[0]; comp.collapsed = comp.border = false; if (me.fill) { comp.flex = 1; } } // Render all Panels. me.callParent(arguments); // Postprocess rendered Panels. ln = renderedPanels.length; for (i = 0; i < ln; i++) { comp = renderedPanels[i]; // Delete the dimension property so that our align: 'stretch' processing manages the width from here delete comp.width; comp.header.addCls(Ext.baseCSSPrefix + 'accordion-hd'); comp.body.addCls(Ext.baseCSSPrefix + 'accordion-body'); } }, onLayout: function() { var me = this; if (me.fill) { me.callParent(arguments); } else { var targetSize = me.getLayoutTargetSize(), items = me.getVisibleItems(), len = items.length, i = 0, comp; for (; i < len; i++) { comp = items[i]; if (comp.collapsed) { items[i].setWidth(targetSize.width); } else { items[i].setSize(null, null); } } } me.updatePanelClasses(); return me; }, updatePanelClasses: function() { var children = this.getLayoutItems(), ln = children.length, siblingCollapsed = true, i, child; for (i = 0; i < ln; i++) { child = children[i]; // Fix for EXTJSIV-3724. Windows only. // Collapsing the Psnel's el to a size which only allows a single hesder to be visible, scrolls the header out of view. if (Ext.isWindows) { child.el.dom.scrollTop = 0; } if (siblingCollapsed) { child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded'); } else { child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded'); } if (i + 1 == ln && child.collapsed) { child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed'); } else { child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed'); } siblingCollapsed = child.collapsed; } }, animCallback: function(){ Ext.Array.forEach(this.toCollapse, function(comp){ comp.fireEvent('collapse', comp); }); Ext.Array.forEach(this.toExpand, function(comp){ comp.fireEvent('expand', comp); }); }, setupEvents: function(){ this.toCollapse = []; this.toExpand = []; }, // When a Component expands, adjust the heights of the other Components to be just enough to accommodate // their headers. // The expanded Component receives the only flex value, and so gets all remaining space. onComponentExpand: function(toExpand) { var me = this, it = me.owner.items.items, len = it.length, i = 0, comp; me.setupEvents(); for (; i < len; i++) { comp = it[i]; if (comp === toExpand && comp.collapsed) { me.setExpanded(comp); } else if (!me.multi && (comp.rendered && comp.header.rendered && comp !== toExpand && !comp.collapsed)) { me.setCollapsed(comp); } } me.animate = me.initialAnimate; if (me.activeOnTop) { // insert will trigger a layout me.owner.insert(0, toExpand); } else { me.layout(); } me.animate = false; return false; }, onComponentCollapse: function(comp) { var me = this, toExpand = comp.next() || comp.prev(), expanded = me.multi ? me.owner.query('>panel:not([collapsed])') : []; me.setupEvents(); // If we are allowing multi, and the "toCollapse" component is NOT the only expanded Component, // then ask the box layout to collapse it to its header. if (me.multi) { me.setCollapsed(comp); // If the collapsing Panel is the only expanded one, expand the following Component. // All this is handling fill: true, so there must be at least one expanded, if (expanded.length === 1 && expanded[0] === comp) { me.setExpanded(toExpand); } me.animate = me.initialAnimate; me.layout(); me.animate = false; } // Not allowing multi: expand the next sibling if possible, prev sibling if we collapsed the last else if (toExpand) { me.onComponentExpand(toExpand); } return false; }, onComponentShow: function(comp) { // Showing a Component means that you want to see it, so expand it. this.onComponentExpand(comp); }, setCollapsed: function(comp) { var otherDocks = comp.getDockedItems(), dockItem, len = otherDocks.length, i = 0; // Hide all docked items except the header comp.hiddenDocked = []; for (; i < len; i++) { dockItem = otherDocks[i]; if ((dockItem !== comp.header) && !dockItem.hidden) { dockItem.hidden = true; comp.hiddenDocked.push(dockItem); } } comp.addCls(comp.collapsedCls); comp.header.addCls(comp.collapsedHeaderCls); comp.height = comp.header.getHeight(); comp.el.setHeight(comp.height); comp.collapsed = true; delete comp.flex; if (this.initialAnimate) { this.toCollapse.push(comp); } else { comp.fireEvent('collapse', comp); } if (comp.collapseTool) { comp.collapseTool.setType('expand-' + comp.getOppositeDirection(comp.collapseDirection)); } }, setExpanded: function(comp) { var otherDocks = comp.hiddenDocked, len = otherDocks ? otherDocks.length : 0, i = 0; // Show temporarily hidden docked items for (; i < len; i++) { otherDocks[i].show(); } // If it was an initial native collapse which hides the body if (!comp.body.isVisible()) { comp.body.show(); } delete comp.collapsed; delete comp.height; delete comp.componentLayout.lastComponentSize; comp.suspendLayout = false; comp.flex = 1; comp.removeCls(comp.collapsedCls); comp.header.removeCls(comp.collapsedHeaderCls); if (this.initialAnimate) { this.toExpand.push(comp); } else { comp.fireEvent('expand', comp); } if (comp.collapseTool) { comp.collapseTool.setType('collapse-' + comp.collapseDirection); } comp.setAutoScroll(comp.initialConfig.autoScroll); } });