/** * This is a layout that manages multiple Panels in an expandable accordion style such that by default only * one Panel can be expanded at any given time (set {@link #multi} config to have more open). 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, * defaults: { * // applied to each contained panel * bodyStyle: 'padding:15px' * }, * layout: { * // layout-specific configs go here * type: 'accordion', * 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', type: 'accordion', alternateClassName: 'Ext.layout.AccordionLayout', targetCls: Ext.baseCSSPrefix + 'accordion-layout-ct', itemCls: [Ext.baseCSSPrefix + 'box-item', Ext.baseCSSPrefix + 'accordion-item'], align: 'stretch', enableSplitters: false, /** * @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. * @removed This config is ignored in ExtJS 4 */ /** * @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. An explicit {@link Ext.panel.Panel#titleCollapse} declared * on the panel will override this setting. */ 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 true. */ 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. By default, this will use the * {@link Ext.panel.Panel#collapseFirst} setting on the panel. If the config option is specified on the layout, * it will override the panel value. */ collapseFirst: undefined, /** * @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, *not* 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 true to enable multiple accordion items to be open at once. */ multi: false, /** * @cfg {Boolean} [wrapOver=true] When `true`, pressing Down or Right arrow key on the * focused last accordion panel header will navigate to the first panel; pressing Up * or Left arrow key on the focused first accordion panel header will navigate to the * last panel. * Set this to `false` to prevent keyboard navigation from wrapping over the edges. */ wrapOver: true, panelCollapseMode: 'header', defaultAnimatePolicy: { y: true, height: true }, constructor: function() { var me = this; me.callParent(arguments); if (me.animate) { me.animatePolicy = {}; /* Animate our parallel dimension and position. So in the default vertical accordion, this will be { y: true, height: true } */ me.animatePolicy[me.names.x] = true; me.animatePolicy[me.names.width] = true; } else { me.animatePolicy = null; } }, beforeRenderItems: function (items) { var me = this, ln = items.length, owner = me.owner, collapseFirst = me.collapseFirst, hasCollapseFirst = Ext.isDefined(collapseFirst), expandedItem = me.getExpanded(true)[0], multi = me.multi, comp, i; for (i = 0; i < ln; i++) { comp = items[i]; if (!comp.rendered) { // Set up initial properties for Panels in an accordion. comp.isAccordionPanel = true; comp.bodyAriaRole = 'tabpanel'; comp.accordionWrapOver = me.wrapOver; if (!multi || comp.collapsible !== false) { comp.collapsible = true; } if (comp.collapsible) { if (hasCollapseFirst) { comp.collapseFirst = collapseFirst; } if (me.hideCollapseTool) { comp.hideCollapseTool = me.hideCollapseTool; comp.titleCollapse = true; } else if (me.titleCollapse && comp.titleCollapse === undefined) { // Only force titleCollapse if we don't explicitly // set one on the child panel comp.titleCollapse = me.titleCollapse; } } comp.hideHeader = comp.width = null; comp.title = comp.title || ' '; comp.addBodyCls(Ext.baseCSSPrefix + 'accordion-body'); // If only one child Panel is allowed to be expanded // then collapse all except the first one found with collapsed:false // If we have hasExpanded set, we've already done this if (!multi) { if (expandedItem) { comp.collapsed = expandedItem !== comp; } else if (comp.hasOwnProperty('collapsed') && comp.collapsed === false) { expandedItem = comp; } else { comp.collapsed = true; } // If only one child Panel may be expanded, then intercept expand/show requests. owner.mon(comp, 'show', me.onComponentShow, me); } // Need to still check this outside multi because we don't want // a single item to be able to collapse comp.headerOverCls = Ext.baseCSSPrefix + 'accordion-hd-over'; } } // If no collapsed:false Panels found, make the first one expanded, only if we're // not during an expand/collapse if (!me.processing && !multi) { if (!expandedItem) { if (ln) { items[0].collapsed = false; } } else if (me.activeOnTop) { expandedItem.collapsed = false; me.configureItem(expandedItem); if (owner.items.indexOf(expandedItem) > 0) { owner.insert(0, expandedItem); } } } }, getItemsRenderTree: function(items) { this.beforeRenderItems(items); return this.callParent(arguments); }, renderItems : function(items, target) { this.beforeRenderItems(items); this.callParent(arguments); }, configureItem: function(item) { this.callParent(arguments); // Accordion headers are immune to dock layout's border-management rules item.ignoreHeaderBorderManagement = true; // We handle animations for the expand/collapse of items. // Items do not have individual borders item.animCollapse = false; // If filling available space, all Panels flex. if (this.fill) { item.flex = 1; } }, beginLayout: function (ownerContext) { this.callParent(arguments); // Accordion widgets have the role of tablist along with the attribute // aria-multiselectable="true" to indicate that it's an accordion // and not just a simple tab panel. // We can't set this role on the panel's main el as this panel may be // a region in a border layout which yields its own set of ARIA attributes. // We also can't set this role on panel's body el, because the panel could be // a FormPanel that would have role="form" on the body el, and the tablist // needs to be contained within it. // innerCt seems to be the most logical choice here. this.innerCt.dom.setAttribute('role', 'tablist'); this.innerCt.dom.setAttribute('aria-multiselectable', true); this.updatePanelClasses(ownerContext); }, updatePanelClasses: function(ownerContext) { var children = ownerContext.visibleItems, ln = children.length, siblingCollapsed = true, i, child, header; for (i = 0; i < ln; i++) { child = children[i]; header = child.header; header.addCls(Ext.baseCSSPrefix + 'accordion-hd'); if (siblingCollapsed) { header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded'); } else { header.addCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded'); } if (i + 1 === ln && child.collapsed) { header.addCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed'); } else { header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed'); } siblingCollapsed = child.collapsed; } }, // 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. onBeforeComponentExpand: function(toExpand) { var me = this, owner = me.owner, multi = me.multi, moveToTop = !multi && !me.animate && me.activeOnTop, expanded, previousValue, anim; if (!me.processing) { me.processing = true; previousValue = owner.deferLayouts; owner.deferLayouts = true; if (!multi) { expanded = me.getExpanded()[0]; if (expanded && expanded !== toExpand) { anim = expanded.$layoutAnim; // If the item is animating, finish it. if (anim) { anim.jumpToEnd(); } expanded.collapse(); } } if (moveToTop) { // Prevent extra layout when moving the item Ext.suspendLayouts(); owner.insert(0, toExpand); Ext.resumeLayouts(); } owner.deferLayouts = previousValue; me.processing = false; } }, onBeforeComponentCollapse: function(comp) { var me = this, owner = me.owner, toExpand, expanded, previousValue; if (me.owner.items.getCount() === 1) { // do not allow collapse if there is only one item return false; } if (!me.processing) { me.processing = true; previousValue = owner.deferLayouts; owner.deferLayouts = true; toExpand = comp.next() || comp.prev(); // 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) { expanded = me.getExpanded(); // 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) { toExpand.expand(); } } else if (toExpand) { toExpand.expand(); } owner.deferLayouts = previousValue; me.processing = false; } }, onComponentShow: function(comp) { this.onBeforeComponentExpand(comp); }, onAdd: function (item) { var me = this; me.callParent(arguments); if (item.collapseMode === 'placeholder') { item.collapseMode = me.panelCollapseMode; } item.collapseDirection = item.headerPosition; // If we add to an accordion after its is has run once we need to make sure // new items are collapsed on entry. The item is also in the collection now, // so only collapse it if we have more than 1. if (me.layoutCount && !me.multi && me.owner.items.getCount() > 1) { // If we get here, we must already have something expanded, so we don't // want to react here. me.processing = true; item.collapse(); me.processing = false; } }, onRemove: function(panel, destroying){ var me = this, item; me.callParent(arguments); if (!me.owner.destroying && !me.multi && !panel.collapsed) { item = me.owner.items.first(); if (item) { item.expand(); } } }, getExpanded: function(explicitCheck){ var items = this.owner.items.items, len = items.length, i = 0, out = [], add, item; for (; i < len; ++i) { item = items[i]; if (!item.hidden) { if (explicitCheck) { add = item.hasOwnProperty('collapsed') && item.collapsed === false; } else { add = !item.collapsed; } if (add) { out.push(item); } } } return out; }, // No need to run an extra layout since everything has already achieved the // desired size when using an accordion. afterCollapse: Ext.emptyFn, afterExpand: Ext.emptyFn});