/**
* This layout manages multiple child Components, each fitted to the Container, where only
* a single child Component can be visible at any given time. This layout style is most commonly
* used for wizards, tab implementations, etc. This class is intended to be extended or created
* via the `layout: 'card'` {@link Ext.container.Container#layout} config, and should generally
* not need to be created directly via the new keyword.
*
* The CardLayout's focal method is {@link #setActiveItem}. Since only one panel is displayed
* at a time, the only way to move from one Component to the next is by calling setActiveItem,
* passing the next panel to display (or its id or index). The layout itself does not provide
* a user interface for handling this navigation, so that functionality must be provided
* by the developer.
*
* To change the active card of a container, call the setActiveItem method of its layout:
*
* @example
* var p = Ext.create('Ext.panel.Panel', {
* layout: 'card',
* items: [
* { html: 'Card 1' },
* { html: 'Card 2' }
* ],
* renderTo: Ext.getBody()
* });
*
* p.getLayout().setActiveItem(1);
*
* The {@link Ext.Component#beforedeactivate beforedeactivate} and
* {@link Ext.Component#beforeactivate beforeactivate} events can be used to prevent a card
* from activating or deactivating by returning `false`.
*
* @example
* var active = 0;
* var main = Ext.create('Ext.panel.Panel', {
* renderTo: Ext.getBody(),
* width: 200,
* height: 200,
* layout: 'card',
* tbar: [{
* text: 'Next',
* handler: function(){
* var layout = main.getLayout();
* ++active;
* layout.setActiveItem(active);
* active = main.items.indexOf(layout.getActiveItem());
* }
* }],
* items: [{
* title: 'P1'
* }, {
* title: 'P2'
* }, {
* title: 'P3',
* listeners: {
* beforeactivate: function(){
* return false;
* }
* }
* }]
* });
*
* In the following example, a simplistic wizard setup is demonstrated. A button bar is added
* to the footer of the containing panel to provide navigation buttons. The buttons will be handled
* by a common navigation routine. Note that other uses of a CardLayout (like a tab control)
* would require a completely different implementation. For serious implementations,
* a better approach would be to extend CardLayout to provide the custom functionality needed.
*
* @example
* var navigate = function(panel, direction){
* // This routine could contain business logic required to manage the navigation steps.
* // It would call setActiveItem as needed, manage navigation button state, handle any
* // branching logic that might be required, handle alternate actions like cancellation
* // or finalization, etc. A complete wizard implementation could get pretty
* // sophisticated depending on the complexity required, and should probably be
* // done as a subclass of CardLayout in a real-world implementation.
* var layout = panel.getLayout();
* layout[direction]();
* Ext.getCmp('move-prev').setDisabled(!layout.getPrev());
* Ext.getCmp('move-next').setDisabled(!layout.getNext());
* };
*
* Ext.create('Ext.panel.Panel', {
* title: 'Example Wizard',
* width: 300,
* height: 200,
* layout: 'card',
* bodyStyle: 'padding:15px',
* defaults: {
* // applied to each contained panel
* border: false
* },
* // just an example of one possible navigation scheme, using buttons
* bbar: [
* {
* id: 'move-prev',
* text: 'Back',
* handler: function(btn) {
* navigate(btn.up("panel"), "prev");
* },
* disabled: true
* },
* '->', // greedy spacer so that the buttons are aligned to each side
* {
* id: 'move-next',
* text: 'Next',
* handler: function(btn) {
* navigate(btn.up("panel"), "next");
* }
* }
* ],
* // the panels (or "cards") within the layout
* items: [{
* id: 'card-0',
* html: '<h1>Welcome to the Wizard!</h1><p>Step 1 of 3</p>'
* },{
* id: 'card-1',
* html: '<p>Step 2 of 3</p>'
* },{
* id: 'card-2',
* html: '<h1>Congratulations!</h1><p>Step 3 of 3 - Complete</p>'
* }],
* renderTo: Ext.getBody()
* });
*/
Ext.define('Ext.layout.container.Card', {
extend: 'Ext.layout.container.Fit',
alternateClassName: 'Ext.layout.CardLayout',
alias: 'layout.card',
type: 'card',
hideInactive: true,
/**
* @cfg {Boolean} deferredRender
* `true` to render each contained item at the time it becomes active, `false` to render
* all contained items as soon as the layout is rendered (defaults to false). If there is
* a significant amount of content or a lot of heavy controls being rendered into panels
* that are not displayed by default, setting this to `true` might improve performance.
*/
deferredRender: false,
getRenderTree: function() {
var me = this,
activeItem = me.getActiveItem();
if (activeItem) {
// If they veto the activate, we have no active item
if (activeItem.hasListeners.beforeactivate &&
activeItem.fireEvent('beforeactivate', activeItem) === false) {
// We must null our activeItem reference, AND the one in our owning Container.
// Because upon layout invalidation, renderChildren will use this.getActiveItem
// which uses this.activeItem || this.owner.activeItem
activeItem = me.activeItem = me.owner.activeItem = null;
}
// Item is to be the active one. Fire event after it is first layed out
else if (activeItem.hasListeners.activate) {
activeItem.on({
boxready: function() {
activeItem.fireEvent('activate', activeItem);
},
single: true
});
}
if (me.deferredRender) {
if (activeItem) {
return me.getItemsRenderTree([activeItem]);
}
}
else {
return me.callParent(arguments);
}
}
},
renderChildren: function() {
var me = this,
active = me.getActiveItem();
if (!me.deferredRender) {
me.callParent();
}
else if (active) {
// ensure the active item is configured for the layout
me.renderItems([active], me.getRenderTarget());
}
},
isValidParent: function(item, target, position) {
// Note: Card layout does not care about order within the target because only one
// is ever visible.
// We only care whether the item is a direct child of the target.
var itemEl = item.el ? item.el.dom : Ext.getDom(item);
return (itemEl && itemEl.parentNode === (target.dom || target)) || false;
},
/**
* Return the active (visible) component in the layout.
* @return {Ext.Component}
*/
getActiveItem: function() {
var me = this,
// It's necessary to check that me.activeItem is not undefined
// as it could be 0 (falsey). We're more interested in checking
// the layout's activeItem property, since that is the source of truth
// for an activeItem. If it's determined to be empty, check the owner.
// Note that a default item is returned if activeItem is `undefined` but not `null`.
// Also, note that `null` is legitimate value and completely different from `undefined`.
item = me.activeItem === undefined ? (me.owner && me.owner.activeItem) : me.activeItem,
result = me.parseActiveItem(item);
// Sanitize the result in case the active item is no longer there.
if (result && me.owner.items.indexOf(result) !== -1) {
me.activeItem = result;
}
// Note that in every use case me.activeItem will have a truthy value except for
// when a container or tabpanel is explicity configured with activeItem/Tab === null
// or when an out-of-range index is given for an active tab (as it will be undefined).
// In those cases, it is meaningful to return the null value, so do so.
return result == null ? null : (me.activeItem || me.owner.activeItem);
},
/**
* @private
*/
parseActiveItem: function(item) {
var activeItem;
if (item && item.isComponent) {
activeItem = item;
}
else if (typeof item === 'number' || item === undefined) {
activeItem = this.getLayoutItems()[item || 0];
}
else if (item === null) {
activeItem = null;
}
else {
activeItem = this.owner.getComponent(item);
}
return activeItem;
},
/**
* @private
* Called before both dynamic render, and bulk render.
* Ensure that the active item starts visible, and inactive ones start invisible.
*/
configureItem: function(item) {
item.setHiddenState(item !== this.getActiveItem());
this.callParent(arguments);
},
onAdd: function(item, pos) {
this.callParent([item, pos]);
this.setItemHideMode(item);
},
onRemove: function(component) {
var me = this;
me.callParent([component]);
me.resetItemHideMode(component);
if (component === me.activeItem) {
// Note setting to `undefined` is intentional. Don't null it out since null
// now has a specific meaning in tab management (it specifies not setting
// an active item).
me.activeItem = undefined;
}
},
/**
* @private
*/
getAnimation: function(newCard, owner) {
var newAnim = (newCard || {}).cardSwitchAnimation;
if (newAnim === false) {
return false;
}
return newAnim || owner.cardSwitchAnimation;
},
/**
* Return the active (visible) component in the layout to the next card
* @return {Ext.Component} The next component or false.
*/
getNext: function() {
var wrap = arguments[0],
items = this.getLayoutItems(),
index = Ext.Array.indexOf(items, this.activeItem);
return items[index + 1] || (wrap ? items[0] : false);
},
/**
* Sets the active (visible) component in the layout to the next card
* @return {Ext.Component} the activated component or false when nothing activated.
*/
next: function() {
var anim = arguments[0],
wrap = arguments[1];
return this.setActiveItem(this.getNext(wrap), anim);
},
/**
* Return the active (visible) component in the layout to the previous card
* @return {Ext.Component} The previous component or false.
*/
getPrev: function() {
var wrap = arguments[0],
items = this.getLayoutItems(),
index = Ext.Array.indexOf(items, this.activeItem);
return items[index - 1] || (wrap ? items[items.length - 1] : false);
},
/**
* Sets the active (visible) component in the layout to the previous card
* @return {Ext.Component} the activated component or false when nothing activated.
*/
prev: function() {
var anim = arguments[0],
wrap = arguments[1];
return this.setActiveItem(this.getPrev(wrap), anim);
},
/**
* Makes the given card active.
*
* var card1 = Ext.create('Ext.panel.Panel', {itemId: 'card-1'});
* var card2 = Ext.create('Ext.panel.Panel', {itemId: 'card-2'});
* var panel = Ext.create('Ext.panel.Panel', {
* layout: 'card',
* activeItem: 0,
* items: [card1, card2]
* });
* // These are all equivalent
* panel.getLayout().setActiveItem(card2);
* panel.getLayout().setActiveItem('card-2');
* panel.getLayout().setActiveItem(1);
*
* @param {Ext.Component/Number/String} newCard The component, component
* {@link Ext.Component#id id}, {@link Ext.Component#itemId itemId}, or index of component.
* @return {Ext.Component} the activated component or false when nothing activated.
* False is returned also when trying to activate an already active card.
*/
setActiveItem: function(newCard) {
var me = this,
owner = me.owner,
oldCard = me.activeItem,
rendered = owner.rendered,
newIndex, focusNewCard;
newCard = me.parseActiveItem(newCard);
newIndex = owner.items.indexOf(newCard);
// If the card is not a child of the owner, then add it.
// Without doing a layout!
if (newIndex === -1) {
newIndex = owner.items.items.length;
Ext.suspendLayouts();
newCard = owner.add(newCard);
Ext.resumeLayouts();
}
// Is this a valid, different card?
if (newCard && oldCard !== newCard) {
// Fire the beforeactivate and beforedeactivate events on the cards
if (newCard.fireEvent('beforeactivate', newCard, oldCard) === false) {
return false;
}
if (oldCard && oldCard.fireEvent('beforedeactivate', oldCard, newCard) === false) {
return false;
}
if (rendered) {
Ext.suspendLayouts();
// If the card has not been rendered yet, now is the time to do so.
if (!newCard.rendered) {
me.renderItem(newCard, me.getRenderTarget(), owner.items.length);
}
if (oldCard) {
if (me.hideInactive) {
focusNewCard = oldCard.el.contains(Ext.Element.getActiveElement());
oldCard.hide();
if (oldCard.hidden) {
oldCard.hiddenByLayout = true;
oldCard.fireEvent('deactivate', oldCard, newCard);
}
// Hide was vetoed, we cannot change cards.
else {
return false;
}
}
}
// Make sure the new card is shown
if (newCard.hidden) {
newCard.show();
}
// Layout needs activeItem to be correct, so clear it if the show has been vetoed,
// set it if the show has *not* been vetoed.
if (newCard.hidden) {
me.activeItem = newCard = null;
}
else {
me.activeItem = newCard;
// If the card being hidden contained focus, attempt to focus the new card
// So as not to leave focus undefined.
// The focus() call will focus the defaultFocus if it is a container
// so ensure there is a defaultFocus.
if (focusNewCard) {
if (!newCard.defaultFocus) {
newCard.defaultFocus = ':focusable';
}
newCard.focus();
}
}
Ext.resumeLayouts(true);
}
else {
me.activeItem = newCard;
}
newCard.fireEvent('activate', newCard, oldCard);
return me.activeItem;
}
return false;
},
/**
* @private
* Reset back to initial config when item is removed from the panel.
*/
resetItemHideMode: function(item) {
item.hideMode = item.originalHideMode;
delete item.originalHideMode;
},
/**
* @private
* A card layout items must have its visibility mode set to OFFSETS so its scroll
* positions isn't reset when hidden.
*
* Do this automatically when an item is added to the panel.
*/
setItemHideMode: function(item) {
item.originalHideMode = item.hideMode;
item.hideMode = 'offsets';
}
});